Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock DbContext SaveChanges() method does not return any values

I was wondering if anyone knew of a way to cause SaveChanges() to return a value other than 0 when mocking a DbContext using Moq. I frequently use SaveChanges() in my code to throw errors if the number of items changes Saved or UnSaved is less than or greater than the expected number of changes.

In my experience using Moq, it doesn't look like DbContext.SaveChanges() will ever do anything if you aren't making calls to the database.

Update 9/8/2016

Suggestions from Jeroen Heier and Nkosi have helped provide a solution. I should have dug deeper into Mock and realized that SaveChanges() was a virtual (duh!). For reference I'll add the code below to demo what I'm doing. One thing to note is that you need to define a callback to save changes if your business logic depends on the number of items that are saved to the database DbContext.Setup().Callback(() => { /* do something */}). Using the callback you can keep track of the expected number of times save changes is called and Assert them in your test.

"SavePost" Method in the PostService class

public PostViewModel SavePost(PostViewModel currentPost, bool publishPost)
{
    //_context.Configuration.ProxyCreationEnabled = false;

    JustBlogContext currentContext = (JustBlogContext)_Context;
    Post newPost = new Post();
    List<Tag> postTags = new List<Tag>();
    DateTime currentDateTime = DateTime.UtcNow;
    bool saveSuccess = false;

    if (currentPost != null)
    {
        //Post
        newPost = new Post()
        {
            Category = currentPost.Category.Id.Value,
            Description = currentPost.Description,
            ShortDescription = currentPost.ShortDescription,
            Title = currentPost.Title,
            UrlSlug = currentPost.UrlSlug,
            Published = currentPost.Published == true || publishPost == true ? true : false,
            Meta = currentPost.Meta == "" || currentPost.Meta == null ? "No meta data" : currentPost.Meta,
            PostedOn = currentPost.PostedOn,
            Modified = currentDateTime
        };

        //Tags
        foreach (Tag currentTag in currentPost?.Tags)
        {
            postTags.Add(new Tag()
            {
                Description = currentTag.Description,
                Id = currentTag.Id,
                Name = currentTag.Name,
                UrlSlug = currentTag.UrlSlug
            });
        }

        if (currentPost.PostedOn == null && publishPost)
        {
            newPost.PostedOn = currentDateTime;
        }

        /**
         * Note that you must track all entities
         * from the Post side of the Post - PostTagMap - Tag database schema.
         * If you incorrectly track entites you will add new tags as opposed to
         * maintaining the many-to-many relationship.
         **/
        if (currentPost?.Id == null)
        {
            //Add a new post
            //Attach tags from the database to post
            foreach (Tag clientTag in postTags)
            {
                if (currentContext.Entry(clientTag).State == EntityState.Detached)
                {
                    currentContext.Tags.Attach(clientTag);
                }
            }

            newPost.Tags = postTags;
            currentContext.Posts.Add(newPost);
            saveSuccess = currentContext.SaveChanges() > 0 ? true : false;
        }
        else
        {
            //Modify and existing post.
            bool tagsModified = false;
            newPost.Id = currentPost.Id.Value;
            currentContext.Posts.Attach(newPost);
            currentContext.Entry(newPost).State = EntityState.Modified;

            saveSuccess = currentContext.SaveChanges() > 0 ? true : false;
            List<Tag> dataTags = currentContext.Posts.Include(post => post.Tags).FirstOrDefault(p => p.Id == newPost.Id).Tags.ToList();

            //Remove old tags
            foreach (Tag tag in dataTags)
            {
                if (!postTags.Select(p => p.Id).ToList().Contains(tag.Id))
                {
                    tagsModified = true;
                    newPost.Tags.Remove(tag);
                }
            }

            if (postTags.Count() > 0)
            {
                //Add new tags
                foreach (Tag clientTag in postTags)
                {
                    //Attach each tag because it comes from the client, not the database
                    if (!dataTags.Select(p => p.Id).ToList().Contains(clientTag.Id))
                    {
                        currentContext.Tags.Attach(clientTag);
                    }

                    if (!dataTags.Select(p => p.Id).ToList().Contains(clientTag.Id))
                    {
                        tagsModified = true;
                        newPost.Tags.Add(currentContext.Tags.Find(clientTag.Id));
                    }
                }

                //Only save changes if we modified the tags
                if (tagsModified)
                {
                    saveSuccess = currentContext.SaveChanges() > 0 ? true : false;
                }
            }
        }
    }

    if (saveSuccess != false)
    {
        return loadEditedPost(currentContext, newPost);
    }
    else
    {
        throw new JustBlogException($"Error saving changes to {newPost.Title}");
    }
}

"SavePost_New_Post_Test" Test Method in PostService_Test.cs

/// <summary>
/// Test saving a new post
/// </summary>
[TestMethod]
public void SavePost_New_Post_Test()
{
    //Arrange
    var postSet_Mock = new Mock<DbSet<Post>>();

    var justBlogContext_Mock = new Mock<JustBlogContext>();
    justBlogContext_Mock.Setup(m => m.Posts).Returns(postSet_Mock.Object);

    IPostService postService = new PostService(justBlogContext_Mock.Object);

    // setup Test
    CategoryViewModel newCategory = new CategoryViewModel()
    {
        Description = "Category Description",
        Id = 0,
        Modified = new DateTime(),
        Name = "Name",
        PostCount = 0,
        Posts = new List<Post>(),
        UrlSlug = "Category Url Slug"
    };

    Tag newTag = new Tag()
    {
        Description = "Tag Description",
        Id = 1,
        Modified = new DateTime(),
        Name = "Tag Name",
        Posts = new List<Post>(),
        UrlSlug = "Url Slug"
    };

    Tag newTag2 = new Tag()
    {
        Description = "Tag Description 2",
        Id = 2,
        Modified = new DateTime(),
        Name = "Tag Name 2",
        Posts = new List<Post>(),
        UrlSlug = "UrlSlug2"
    };

    List<Tag> tags = new List<Tag>();

    tags.Add(newTag);
    tags.Add(newTag2);

    // setup new post
    PostViewModel newPost = new PostViewModel()
    {
        Category = newCategory,
        Description = "Test Descritpion",
        Meta = "Meta text",
        Modified = new DateTime(),
        Published = false,
        PostedOn = null,
        ShortDescription = "Short Description",
        Title = "TestTitle",
        UrlSlug = "TestUrlSlug",
        Tags = tags,
        Id = null
    };

    // counters to verify call order
    int addPostCount = 0;
    int addTagAttachCount = 0;
    int saveChangesCount = 0;
    int totalActionCount = 0;

    // Register callbacks to keep track of actions that have occured
    justBlogContext_Mock.Setup(x => x.Posts.Add(It.IsAny<Post>())).Callback(() =>
    {
        addPostCount++;
        totalActionCount++;
    });

    justBlogContext_Mock.Setup(x => x.SaveChanges()).Callback(() =>
    {
        saveChangesCount++;
        totalActionCount++;
    });

    justBlogContext_Mock.Setup(x => x.SaveChanges()).Returns(1);

    justBlogContext_Mock.Setup(x => x.Tags.Attach(It.IsAny<Tag>())).Callback(() =>
    {
        addTagAttachCount++;
        totalActionCount++;
    });          

    // Act
    postService.SavePost(newPost, false);
    // Assert
    justBlogContext_Mock.Verify(m => m.SaveChanges(), Times.AtLeastOnce());
    Assert.IsTrue(addPostCount == 1);
    Assert.IsTrue(addTagAttachCount == 2);
    Assert.IsTrue(totalActionCount == 4);
}
like image 313
Brian Mikinski Avatar asked Sep 08 '16 04:09

Brian Mikinski


People also ask

What does the Dbcontext SaveChanges () method return?

Returns. The number of state entries written to the underlying database. This can include state entries for entities and/or relationships.

What SaveChanges return value in Entity Framework?

SaveChanges() always returns 0 – Entity Framework According to EF documentation, SaveChanges() should return a number of affected rows.

What is SaveChanges method?

In Entity Framework, the SaveChanges() method internally creates a transaction and wraps all INSERT, UPDATE and DELETE operations under it. Multiple SaveChanges() calls, create separate transactions, perform CRUD operations and then commit each transaction.

How do you mock a transaction?

To do this, you can simply mock the transaction templates method to execute whatever is inside the block. The code above uses Mockito Kotlin . An easy way to execute any call inside the execute block is setup a @BeforeEach function from JUnit that will get called before each test execution.


1 Answers

Assuming an interface/abstraction like

public interface IDbContext {
    int SaveChanges();
}

You would setup the mock like this.

var expected = 3;
var mock = new Mock<IDbContext>();
mock.Setup(m => m.SaveChanges()).Returns(expected);


var context = mock.Object;

var actual = context.SaveChanges();

Assert.AreEqual(expected, actual);
like image 186
Nkosi Avatar answered Sep 29 '22 23:09

Nkosi