How to write unit tests with Entity Framework Core?

How to write unit tests with Entity Framework Core?

Introduction

The unit test will test only the SUT(system under testing) behavior and correctness and it will not test the SUT’s collaborators You might raise a question how to manage dependencies or eliminate them from the unit test, it is a very good question but to answer for this question is very hard. Fortunately, we have test double techniques to manage SUT dependencies. Test Double is out of the scope for this post and you can refer this link to get more information on Test Double.

In this article, we going to see how to write a unit test against Entity Framework Core. As you are aware, EF is one popular ORM framework in .Net platform. EF comes with a default option to enable to in-memory store and it simulates fake test double. Out of the box, you can use mock frameworks to create any flavors of test double.

We are going to see both options in this article and we going to take a common example for both options. Assume you have an e-commerce site in ASP.Net Core and it has product management along with other features. So, you have ProductsController, ProductRepository and EF Db Context and code implementation look like as below,

Product Controller:

namespace Wingtip.Toys.ECommerce.Controllers
{
    [Route("products")]
    public class ProductsController : Controller
    {
        private readonly IProductRepository _productRepository;

        public ProductsController(IProductRepository productRepository)
        {
            _productRepository = productRepository;
        }
        [Route("list")]
        public IActionResult List()
        {
          
            var products = _productRepository.GetAll();
            return View(products);
        }
        
        [Route("{category}")]
        public IActionResult Index(string category)
        {
            
            var products = _productRepository.GetByCategoryName(category);
            return View(products);
        }
        
        [Route("details/{id}")]
        public IActionResult Details(int id)
        {
            var product = _productRepository.GetById(id);
            return View(product);
        }

        
    }
}

Repository Contracts:

namespace Wingtip.Toys.Data.Contract
{
    public interface IRepository<T>
    {
        T GetById(int id);
        T Add(T t);
        bool Update(T t);
        bool Delete(int id);
        IEnumerable<T> GetAll();
    }
}

namespace Wingtip.Toys.Data.Contract
{
    public interface IProductRepository : IRepository<Product>
    {
        IEnumerable<Product> GetByCategoryName(string category);
    }
}

Product Repository:

namespace WingTip.Toys.Data.Repository
{
    public class ProductRepository : IProductRepository
    {
        private readonly WingtipToysDbContext _dbContext;
        public ProductRepository()
        {
            _dbContext = new WingtipToysDbContext();
        }
        public ProductRepository(WingtipToysDbContext dbContext)
        {
            _dbContext = dbContext;
        }
        public Product GetById(int id)
        {
            return _dbContext.Products.Include("Category").First(p => p.ProductID == id);
        }

        public Product Add(Product product)
        {
            var result = _dbContext.Products.Add(product);
            _dbContext.SaveChanges();
            return result.Entity;

        }

        public bool Update(Product product)
        {
            var result = _dbContext.Products.Update(product);
            _dbContext.SaveChanges();
            return result.State == EntityState.Modified;
        }

        public bool Delete(int id)
        {
            var product = GetById(id);
            if (product == null)
                throw new InvalidOperationException("Product Not Exists!");

            var result = _dbContext.Products.Remove(product);
            _dbContext.SaveChanges();
            return result.State == EntityState.Deleted;
        }

        public IEnumerable<Product> GetAll()
        {
            return _dbContext.Products.Include("Category").ToList();
        }

        public IEnumerable<Product> GetByCategoryName(string category)
        {
            return _dbContext.Products.Include("Category").Where(p => p.Category.CategoryName == category);
        }
    }
}

Db Context:

public class WingtipToysDbContext : DbContext
{

    public WingtipToysDbContext()
    {

    }

    public WingtipToysDbContext(DbContextOptions dbContextOptions):base(dbContextOptions)
    {

    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if(!optionsBuilder.IsConfigured)
            optionsBuilder.UseSqlServer("<connection string goes here>");
    }
    public virtual DbSet<Category> Categories { get; set; }

    public virtual DbSet<Product> Products { get; set; }
}

In Memory Database

In memory, option creates and connects to an in-memory fake database which helps us to write unit tests without violating the FIRST property of unit test. This feature available in  Microsoft.Entityframework.InMemory NuGet package so you need to add this package into your test project if you don’t have already. the second pre-requisite is your Db context class should accept the DbContextOption through a constructor. The third is optional, you should check already option configured for connection string when you are setting the connection string on OnConfiguring event.  You should use the following snippet to enable the in-memory option once you added the required package into your project.

var options = new DbContextOptionsBuilder<WingtipToysDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString("N")).Options;

var dbContext = new WingtipToysDbContext(options);

Here, we need to use the UseInMemoryDatabase extension function of DbContextOptionsBuilder. This function takes the database name as an argument and you can provide any name whatever you want. In this example, I used the Guid to set a unique name for the database. Assume, we have following test cases that product repository needs to stratify and let’s write required unit tests for these test cases with EF in-memory database,

  1. It should add a product successfully into the data store
  2. It should update a product successfully into the data store
  3. It should remove a product successfully from the data store

Unit tests:

[TestClass]
    public class ProductRepositoryTest
    {
        private WingtipToysDbContext CreateDbContext()
        {
            var options = new DbContextOptionsBuilder<WingtipToysDbContext>()
                .UseInMemoryDatabase(Guid.NewGuid().ToString("N")).Options;

            var dbContext = new WingtipToysDbContext(options);
            return dbContext;
        }

        

        [TestMethod]
        public void It_should_add_a_product_successfully_into_data_store()
        {
            //Arrange
            var dbContext = CreateDbContext();
            var sut = new ProductRepository(dbContext);
            var product = new Product { Description = "Model S - Tesla",ProductName="Tesla Car - Model S",UnitPrice=10000 };

            //Act
            var result = sut.Add(product);
           
            //Assert
            Assert.IsTrue(result.ProductID > 0);

            //Clean up
            dbContext.Dispose();
        }

        [TestMethod]
        public void It_should_update_a_product_successfully_into_data_store()
        {
            //Arrange
            var expected = 20000;
            var dbContext = CreateDbContext();
            var sut = new ProductRepository(dbContext);
            var product = new Product { Description = "Model S - Tesla", ProductName = "Tesla Car - Model S", UnitPrice = 10000 };

            //Act
            var result = sut.Add(product);
            result.UnitPrice = expected;
            sut.Update(result);
            result = dbContext.Products.First(p=>p.ProductID == result.ProductID);

            //Assert
            Assert.AreEqual(expected,result.UnitPrice);

            //Clean up
            dbContext.Dispose();
        }

        [TestMethod]
        public void It_should_remove_a_product_successfully_from_the_data_store()
        {
            //Arrange
            var dbContext = CreateDbContext();
            var sut = new ProductRepository(dbContext);
            var product = new Product { Description = "Model S - Tesla", ProductName = "Tesla Car - Model S", UnitPrice = 10000 };

            //Act
            var result = sut.Add(product);
            sut.Delete(result.ProductID);
            var isExists = dbContext.Products.Any(p => p.ProductID == result.ProductID);

            //Assert
            Assert.IsFalse(isExists);

            //Clean up
            dbContext.Dispose();
        }

    }

In this example, CreateDbContext method will enable in-memory database with the name of a Guid then creates and returns a Db context object with the in-memory option. Whenever you call this method, it will create a new Db context object with a new in-memory database instead of the real database. Once you created the Db context object then you can do whatever you want on the context object.

Using Mock Frameworks

Alternatively, you can use the mock frameworks to create test double and do the unit tests without using the real database. In this example, I am using the Moq which is one of a popular mock framework for .Net platform. Before start writing the unit test you should ensure all of Db context’s DbSet properties marked as virtual then only Moq framework create a fake object and override these properties. You must do the following steps by using your mock framework,

  1. Create a generic collection for each DbSet properties
  2. Configure you fake DbSet in your mock framework
  3. Setup to forward following DbSet properties and methods to the given generic collection
    • Provider
    • Expression
    • ElementType
    • GetEnumerator()

Let’s continue to write some more unit tests for following test cases for Product Repository,

  1. It should throw invalid operation exception when user trying to delete non-existing product
  2. It should list all available products from the data store
  3. It should return a product for given product id
  4. It should return a list of products for the given product category name
[TestClass]
    public class ProductRepositoryTest
    {
        private DbSet<T> CreateDbSet<T>(IQueryable<T> collection) where T:class
        {
            var stubDbSet = new Mock<DbSet<T>>();
            stubDbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(collection.Provider);
            stubDbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(collection.Expression);
            stubDbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(collection.ElementType);
            stubDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(collection.GetEnumerator());
            return stubDbSet.Object;
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidOperationException))]
        public void It_should_throw_invalid_operation_exception_when_try_to_delete_a_non_existing_product()
        {
            //Arrange
            var collection = new List<Product>
            {
                new Product {ProductName ="Tesla Model 3",ProductID=1},
                new Product {ProductName ="Tesla Model S",ProductID=2},
                new Product {ProductName ="Tesla Model X",ProductID=3}
            };

            var productDbSet = CreateDbSet(collection.AsQueryable());

            var stubContext = new Mock<WingtipToysDbContext>();
            stubContext.Setup(o => o.Products).Returns(productDbSet);
            var sut = new ProductRepository(stubContext.Object);

            //Act
            var actual = sut.Delete(4);

            
        }

        [TestMethod]
        public void It_should_list_all_available_products_from_the_data_store()
        {
            //Arrange
            var expected = new List<Product>
            {
                new Product {ProductName ="Tesla Model 3"},
                new Product {ProductName ="Tesla Model S"},
                new Product {ProductName ="Tesla Model X"}
            };

            var productDbSet = CreateDbSet(expected.AsQueryable());

            var stubContext = new Mock<WingtipToysDbContext>();
            stubContext.Setup(o=>o.Products).Returns(productDbSet);
            var sut = new ProductRepository(stubContext.Object);

            //Act
            var actual = (List<Product>)sut.GetAll();

            //Assert
            CollectionAssert.AreEquivalent(expected, actual);

        }


        [TestMethod]
        public void It_should_return_a_product_for_given_product_id()
        {
            //Arrange
            var expected = "Tesla Model S";
            var collection = new List<Product>
            {
                new Product {ProductName ="Tesla Model 3",ProductID=1},
                new Product {ProductName ="Tesla Model S",ProductID=2},
                new Product {ProductName ="Tesla Model X",ProductID=3}
            };

            var productDbSet = CreateDbSet(collection.AsQueryable());

            var stubContext = new Mock<WingtipToysDbContext>();
            stubContext.Setup(o => o.Products).Returns(productDbSet);
            var sut = new ProductRepository(stubContext.Object);

            //Act
            var actual = sut.GetById(2);

            //Assert
            Assert.AreEqual(expected, actual.ProductName);

        }

        [TestMethod]
        public void It_should_return_list_of_product_for_given_product_category_name()
        {
            //Arrange
            var products = new List<Product>
            {
                new Product {ProductName ="Tesla Model 3",ProductID=1,Category = new Category {CategoryName ="Car",CategoryID=1}},
                new Product {ProductName ="Tesla Model S",ProductID=2,Category = new Category {CategoryName ="Car",CategoryID=1}},
                new Product {ProductName ="Tesla Model X",ProductID=3,Category = new Category {CategoryName ="Car",CategoryID=1}}
            };

            var productDbSet = CreateDbSet(products.AsQueryable());
            var stubContext = new Mock<WingtipToysDbContext>();
            stubContext.Setup(o => o.Products).Returns(productDbSet);
            var sut = new ProductRepository(stubContext.Object);

            //Act
            var actual = sut.GetByCategoryName("Car").ToList();

            //Assert
            CollectionAssert.AreEquivalent(products, actual);

        }
    }

In the above example, I have used the CreateDbSet method which creates the stub for DbSet and set up the call forward for some of the DbSet’s properties and function to the given collection. Same way you can create other test doubles. I hope you find something useful from this article, please share your comments.

Leave a Reply

Your email address will not be published. Required fields are marked *