Unit Testing with FakeDBSet and NBuilder

8 minute read

Introduction

I have mentioned in a previous blog post that I make use of NBuilder and Faker.NET to create test data for my application in development and testing scenarios.

I also make use of NBuilder to set up scenarios for testing queries against my DbContext to ensure that the correct data is returned from a query.

In this blog post I am going to demonstrate how you can test an ASP.NET MVC controller by using a mock DbContext and a fake DbSet to test that the controller actions behave correctly.

Oh yeah, and for those people who find the use of DbContext inside your MVC controller abhorrent, the concepts I am demonstrating is still valid whether you use a DbContext in your repository or service layer or whatever other architecture you are using.

Please don’t write me again about how stupid I am to use a DbContext in my controller… :)

The basic application

For the base application I have a simple product listing which displays a list of products in the database. The list have a basic filter which allows the user to include or exclude discontinued products from the list:

When a user clicks on the name of a product, they are navigated to the detail screen where they can view the details of a particular product:

Nothing fancy, and I basically just created it using the MVC scaffolding inside Visual Studio, and got rid of the controller actions which allows user to add, edit or delete products as I wanted to limit the scope of this blog post.

This is what the product controller looks like:

public class ProductsController : Controller
{
    private readonly ApplicationDbContext _dbContext;

    public ProductsController()
    {
        _dbContext = new ApplicationDbContext();
    }

    public ActionResult Index(bool includeDiscontinued = false)
    {
        // Set base product query
        var productsQuery = _dbContext.Products.AsQueryable();

        // Filter out discontinued products
        if (!includeDiscontinued)
            productsQuery = productsQuery.Where(p => p.IsDiscontinued == false);

        // Create view model
        var viewModel = new ProductIndexViewModel
        {
            IncludeDiscontinued = includeDiscontinued,
            Products = productsQuery.ToList()
        };

        // Display index view
        return View(viewModel);
    }

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

For the Index action I get the list of products, and filter out the discontinued once in case the user does not want to view discontinued products (which is the default).

For the Details action I display the product with some checks to see whether an id was passed it, and also checking to see whether the product exists and returning appropriate HTTP status codes if either case is not true. This is the basic behaviour generated by the MVC scaffolding, and I just left it as-is.

For completeness sake, here is my current ApplicationDbContext and Product class:

public class ApplicationDbContext : DbContext
{
    public virtual DbSet<Product> Products { get; set; }

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

public class Product
{
    public string Description { get; set; }
	[Key]
    public int Id { get; set; }
    public string ImageUrl { get; set; }
    public bool IsDiscontinued { get; set; }
    public string Name { get; set; }
    public float Price { get; set; }
}

Making the DbContext testable

To make the DbContext testable, I will need to extract an interface from the ApplicationDbContext so I can mock the interface. I will also change the type of the Products DbSet from DbSet<Products> to IDbSet<Products> - once again to better aid with testing:

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")
    {
    }
}

In the controller I will change the data type of the _dbContext from ApplicationDbContext to IApplicationDbContext. I also added an extra constructor overload which allows me to pass in an instance of IApplicationDbContext. This will be used by my unit tests to pass in the mock database context.

public class ProductsController : Controller
{
    private readonly IApplicationDbContext _dbContext;

    public ProductsController() : this(new ApplicationDbContext())
    {
            
    }

    public ProductsController(IApplicationDbContext dbContext)
    {
        _dbContext = dbContext;
    }

	...
}

In a production scenario you will probably ensure that an instance of IApplicationDbContext is injected into the controller using an IoC container like Autofac.

Adding NSubstitute and FakeDbSet

For the unit test I have created a unit test project and added a reference to the main ASP.NET MVC project. You will also need to install some Nuget packages for the unit test project:

install-package EntityFramework
install-package NBuilder
install-package NSubstitute
Install-Package TestStack.FluentMVCTesting

Entity Framework is needed for the FakeDbSet class I will add, NSubstitute is used as the mocking framework, and NBuilder will be used to create the actual fake data.

The FluentMVCTesting package allows me to unit test my controllers in a fluent way. For more details have a look at the blog post called Testing ASP.Net MVC Controllers with FluentMVCTesting by Jason Roberts.

I use the FakeDbSet implementation in this blog post by David Walker. Copy the code for his class and add it to your unit test project.

One caveat I would like to point out with this implementation of FakeDbSet is that your will need to decorate the primary key of your model with the [Key] attribute, otherwise the Find() method will not work.

There are various other implementations of a fake DbSet on the internet and your can Google for the term “FakeDbSet C#” to see some of the others. Pick one you like.

Creating test scenarios

For the unit tests I will create a scenario where there is a list of 20 products, with 5 of the products being discontinued.

To create the fake data I use NBuilder. I tell NBuilder to create me a list of 20 products where all products have their IsDiscontinued flag set to false, but to set the IsDiscontinued flag for the first 5 items to true.

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

The reason I have to call to .All().With(p => p.IsDiscontinued = false) is so that I ensure that all the items have a default value of false for IsDiscontinued.

If I don’t do that then NBuilder will randomly assign either true or false to the generated items (other than for the first 5 which I specified explicitely) and therefore my unit tests will not be predictable. I need to know that there are exactly 5 discontinued items in my Products table as that is what I will be checking for in my unit tests.

Next up I will create a mock object for my IApplicationDbContext interface and specify that the mock object returns a FakeDbSet<Product> when the Products property is accessed. I also passed the 20 objects I generated above to the constructor of FakeDbSet as those will be the 20 items which my FakeDbSet will contain.

I also create an instance of ProductsController as the object under test, passing along the mock IApplicationDbContext.

_dbContext = Substitute.For<IApplicationDbContext>();
_dbContext.Products.Returns(new FakeDbSet<Product>(products));

_controller = new ProductsController(_dbContext);

This is what my unit test looks like at this stage:

[TestClass]
public class ProductsControllerTests
{
    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();

        _dbContext = Substitute.For<IApplicationDbContext>();
        _dbContext.Products.Returns(new FakeDbSet<Product>(products));

        _controller = new ProductsController(_dbContext);
    }
}

Testing the Index action

For my index action I am going to write just two unit tests.

The first one will test that when I set the includeDiscontinued flag to true, that my controller will return the default view, passing along an instance of ProductIndexViewModel that contains 20 products. So in other words all the products are returned:

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

The second unit test will check that when I set the includeDiscontinued flag to false that my view will only display the 15 products which are not discontinued:

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

Testing the Details action

For the Details controller action I want to test 3 scenarios:

  1. Test that when the id parameter is null, HTTP status code 400 (Bad Request) is returned.
  2. Test that when I pass in an id for a non-existing product, HTTP status code 404 (Not Found) is returned
  3. Test that when a valid id is passed in, that a the Details view is returned and the correct product (i.e. with the Id I passed in) is passed along as the model for the view.
[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);
}

One thing to note is that when NBuilder generates the list of 20 products for me it will generate the values for the Id property sequentially starting from 1. So in other words, the 20 products will contain a value for Id ranging from 1 through to 20.

I can therefore know what a product with an Id value of 1 does indeed exist, and that a product with and Id value of 21 will not exist.

Conclusion

In this blog post I demonstrated how you can use a Fake DbSet implementation along with a mock DbContext to easily test your MVC controller actions to ensure that the correct results are returned.

Like I said at the beginning, I am using the DbContext direcly inside my MVC controllers. If you use some other service layer or DDD implementation then you probably still interact with the DbContext at some point or another. You can use this technique of using a combination of FakeDbSet and NBuilder to ensure that the interactions with your DbContext at that point is working correctly.

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.