Tom Mikinley Tom Mikinley - 3 months ago 23
C# Question

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.

Thanks in advance for any help!

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);
}

Answer

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);