Unit Testing with NBuilder and NSubstitute using either a FakeDBSet or a mock DbSet

3 minute read

In the previous blog post I showed how you can unit test with NBuilder and NSubstitute by using a FakeDbSet implementation. The thing is that we do not necessarily have to use a FakeDbSet but can also try and mock the DbSet.

Let’s see how we can changes the implementation from last week’s blog post to mock DbSet instead.

First thing to note is that a lot of examples for mocking the DbSet using other mocking frameworks such as Moq (such as this one) will demonstrate using DbSet and IQueryable, but it turns out that people using NSubstitute run into all sort of problems with this, as this SO question demonstrates.

The solution seems to be to use IDbSet throughout. Both for defining the type of your DbSet in the DbContext, and also when mocking the DbSet.

As you may remmeber from last time, I am already using IDbSet on my DbContext:

public interface IApplicationDbContext
{
    IDbSet<Product> Products { get; set; }
    int SaveChanges();
    Task<int> SaveChangesAsync();
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

public class ApplicationDbContext : DbContext, IApplicationDbContext
{
    public virtual IDbSet<Product> Products { get; set; }

    public ApplicationDbContext() : base("DefaultConnection")
    {
    }
}

To demonstrate the difference between the fake and mock DbSet approaches I have created a new set of unit tests from last weeks sample code and copied all the code from the fake DbSet unit tests. The first thing to change is to ensure that the list of products generated by NBuilder returns IQueryable<Product> instead of IList<Product> by using the AsQueryable() extension method:

var products = Builder<Product>.CreateListOfSize(20)
    .All()
    .With(p => p.IsDiscontinued = false)
    .TheFirst(5)
    .With(p => p.IsDiscontinued = true)
    .Build()
    .AsQueryable();

Since we are not using FakeDbSet anymore, I need to mock IDbSet<Product>:

_dbSet = Substitute.For<IDbSet<Product>>();
_dbSet.Provider.Returns(products.Provider);
_dbSet.Expression.Returns(products.Expression);
_dbSet.ElementType.Returns(products.ElementType);
_dbSet.GetEnumerator().Returns(products.GetEnumerator());

and then let the mocking framework return the mock DbSet when the Products are accessed:

_dbContext = Substitute.For<IApplicationDbContext>();
_dbContext.Products.Returns(_dbSet);

I run the unit tests and see that 4 of the unit tests pass, but there is one which is failing:

Let’s look at the code for the controller action:

public ActionResult Details(int? id)
{
    // Ensure an Id is passed in
    if (id == null)
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

    // Get the product from the database
    Product product = _dbContext.Products.Find(id);

    // Ensure the product exists
    if (product == null)
        return HttpNotFound();

    // Display the product details view
    return View(product);
}

It turns out that the call to Find() returns a null Product, and I will need to mock that method on my DbSet as well. To return the correct value for the Find() method of my mock object I used the technique I demonstrated in a previous blog post which generates return values for NSubstitute mock objects based on the calling arguments.

Remember that the signature of the Find() method is as follows:

TEntity Find(params object[] keyValues);

So to return the Product with the Id which is requested by unit test, I simply get the object[] argument from the CallInfo parameter. I then check to see that it was supplied and has a length of 1 (so in other words only one key value was supplied). If so I get the first item from the array and cast it to an int and return the Product from the list of generated products with that Id:

_dbSet.Find(Arg.Any<object[]>()).Returns(callinfo =>
	{
	    object[] idValues = callinfo.Arg<object[]>();
	    if (idValues != null && idValues.Length == 1)
	    {
            int requestedId = (int) idValues[0];
            return products.FirstOrDefault(p => p.Id == requestedId);
	    }
	
	    return null;
	});

And with that small change all the unit tests are passing:

I personally like the FakeDbSet approach as it seems a bit simpler to me, but if you want to you can also use the mock DbSet approach. Whichever you feel most comfortable with.

Here is the complete code for my new unit tests using the mock DbSet approach:

public class ProductsControllerTestsWithMockDbSet
{
    private IDbSet<Product> _dbSet;
    private IApplicationDbContext _dbContext;
    private ProductsController _controller;

    [TestInitialize]
    public void Initialize()
    {
        // Create test product data
        var products = Builder<Product>.CreateListOfSize(20)
            .All()
            .With(p => p.IsDiscontinued = false)
            .TheFirst(5)
            .With(p => p.IsDiscontinued = true)
            .Build()
            .AsQueryable();

        _dbSet = Substitute.For<IDbSet<Product>>();
        _dbSet.Provider.Returns(products.Provider);
        _dbSet.Expression.Returns(products.Expression);
        _dbSet.ElementType.Returns(products.ElementType);
        _dbSet.GetEnumerator().Returns(products.GetEnumerator());
        _dbSet.Find(Arg.Any<object[]>()).Returns(callinfo =>
            {
                object[] idValues = callinfo.Arg<object[]>();
                if (idValues != null && idValues.Length == 1)
                {
                    int requestedId = (int) idValues[0];
                    return products.FirstOrDefault(p => p.Id == requestedId);
                }

                return null;
            });

        _dbContext = Substitute.For<IApplicationDbContext>();
        _dbContext.Products.Returns(_dbSet);

        _controller = new ProductsController(_dbContext);
    }

    #region Index tests

    [TestMethod]
    public void IndexShouldIncludeDiscontinuedProducts()
    {
        _controller.WithCallTo(c => c.Index(true))
            .ShouldRenderDefaultView()
            .WithModel<ProductIndexViewModel>(vm => vm.Products.Count == 20);
    }

    [TestMethod]
    public void IndexShouldNotIncludeDiscontinuedProducts()
    {
        _controller.WithCallTo(c => c.Index(false))
            .ShouldRenderDefaultView()
            .WithModel<ProductIndexViewModel>(vm => vm.Products.Count == 15);
    }

    #endregion

    #region Details tests

    [TestMethod]
    public void DetailsShouldReturnBadRequest()
    {
        _controller.WithCallTo(c => c.Details(null))
            .ShouldGiveHttpStatus(HttpStatusCode.BadRequest);
    }

    [TestMethod]
    public void DetailsShouldReturnNotFound()
    {
        _controller.WithCallTo(c => c.Details(21))
            .ShouldGiveHttpStatus(HttpStatusCode.NotFound);
    }

    [TestMethod]
    public void DetailsShouldReturnCorrectProduct()
    {
        _controller.WithCallTo(c => c.Details(1))
            .ShouldRenderDefaultView()
            .WithModel<Product>(p => p.Id == 1);
    }

    #endregion
 
}

Did you notice an error? Please help me and the other readers by heading over to the GitHub repo for this blog and submit a Pull Request with the corrections.