I’ve discussed using the in-memory database object for Entity Framework in the blog post titled: Dot Net Core In Memory Unit Testing Using xUnit. That was for .Net Core 1.1. Now I’m going to show a larger example of the in-memory database object for .Net Core 2.0. There is only one minor difference in the code between the version 1 and version 2 and it involves the DbContextOptionsBuilder object. The UseInMemoryDatabase() method with no parameters, has been deprecated, so you’ll need to pass a database name into the method. I used my standard “DemoData” database name as a parameter for the sample code in this post. I could have used any variable, because my sample has no queries that reference any particular database name.
My .Net Core 1.1 version unit test code worked for the simple query examples that I used in that demo. Unfortunately, that sample is not “real world.” The unit tests only tested some Linq queries, which serves no real purpose. When I wrote the article, I wanted to show the easiest example of using the in-memory database object and that culminated in the sample that I posted. This time, I’m going to build a couple of business classes and unit test the methods inside those classes. The business classes will contain the Linq queries and perform some functions that are a bit more realistic. Though, this is still a toy program and a contrived example.
I also added a couple of features to provide situations you’ll run into in the real world when using Entity Framework with an IOC container. I did not provide any IOC container code or any project that would execute the EF code against MS SQL server, so this project is still just a unit test project and a business class project.
The Inventory Class
The first class I added was an inventory class. The purpose of this class will be to get information about the inventory of a store. I designed a simple method that requires a store name and the product name and it will return a true if it is in stock:
public class Inventory { private readonly IStoreAppContext _storeAppContext; public Inventory(IStoreAppContext storeAppContext) { _storeAppContext = storeAppContext; } public bool InStock(string storeName, string productName) { var totalItems = (from p in _storeAppContext.Products join s in _storeAppContext.Stores on p.Store equals s.Id where p.Name == productName && s.Name == storeName select p.Quantity).Sum(); return totalItems > 0; } }
As you can see from my class constructor, I am using a pattern that allows me to use an IOC container. This class will need a context to read data from the database and I inject the context for the database into the constructor using an interface. If you’re following the practice of TDD (Test Driven Development), then you’ll create this object with an empty stub for the InStock method and create the unit test(s) first. For my example, I’ll just show the code that I already created and tested. There are a couple of unit tests that are going to be needed for this method. You could probably create some extra edge tests, but I’m just going to create the two obvious tests to see if the query returns a true when the item is in stock and a false if it is not. You can add tests for cases like the store does not exist or the item does not exist. Here are the two unit tests:
[Fact] public void ItemIsInStock() { // Arrange ResetRecords(); var inventory = new Inventory(_storeAppContext); // Act var result = inventory.InStock("J-Foods", "Rice"); // Assert Assert.True(result); } [Fact] public void ItemIsNotInStock() { // Arrange ResetRecords(); var inventory = new Inventory(_storeAppContext); // Act var result = inventory.InStock("J-Foods", "Crackers"); // Assert Assert.False(result); }
Now you’re probably wondering about that method named “ResetRecords()”. Oh that! One of the problems with my previous blog post sample was that I setup the test database data one time in the constructor of the unit test class. Then I ran some unit tests with queries that performed tests that were not destructive to the data. In this sample, I’m going to show how you can test methods that delete data. This test will interfere with other unit tests if the data is not properly restored before each test is run.
Here’s the top part of the unit test class showing the ResetRecords() method:
public class ProductTests : IClassFixture { private readonly StoreAppContext _storeAppContext; private readonly TestDataFixture _fixture; public ProductTests(TestDataFixture fixture) { _fixture = fixture; _storeAppContext = fixture.Context; } private void ResetRecords() { _fixture.ResetData(); }
As you can see, I had to keep track of the fixture object as well as the context. The fixture object was needed in order to access the PopulateData() method that is located inside the fixture class:
public class TestDataFixture : IDisposable { public StoreAppContext Context { get; set; } public TestDataFixture() { var builder = new DbContextOptionsBuilder() .UseInMemoryDatabase("DemoData"); Context = new StoreAppContext(builder.Options); } public void ResetData() { var allProducts = from p in Context.Products select p; Context.Products.RemoveRange(allProducts); var allStores = from s in Context.Stores select s; Context.Stores.RemoveRange(allStores); var store = new Store { Name = "J-Foods" }; Context.Stores.Add(store); Context.Products.Add(new Product { Name = "Rice", Price = 5.99m, Quantity = 5, Store = store.Id }); Context.Products.Add(new Product { Name = "Bread", Price = 2.35m, Quantity = 3, Store = store.Id }); var store2 = new Store { Name = "ToyMart" }; Context.Stores.Add(store2); ((DbContext)Context).SaveChanges(); } public void Dispose() { } }
Notice how I added lines of code to remove ranges of records in both tables before repopulating them. The first pass will not need to delete any data because there is no data. Any calls to the ResetData() method in the TestDataFixture class after the first will need to guarantee that the tables are clean. In your final implementation, I would recommend creating a cs file for each data set you plan to use and follow something similar to the method above to clean and populate your data.
The next method I wanted to implement involved a computation of the total cost of inventory of a store:
public decimal InventoryTotal(string storeName) { var totalCostOfInventory = (from p in _storeAppContext.Products join s in _storeAppContext.Stores on p.Store equals s.Id where s.Name == storeName select p.Price * p.Quantity).Sum(); return totalCostOfInventory ?? 0; }
I came up with two simple unit tests for this method:
[Fact] public void InventoryTotalTest1() { // Arrange ResetRecords(); var inventory = new Inventory(_storeAppContext); // Act var result = inventory.InventoryTotal("J-Foods"); // Assert Assert.Equal(37m, result); } [Fact] public void InventoryTotalEmptyStore() { // Arrange ResetRecords(); var inventory = new Inventory(_storeAppContext); // Act var result = inventory.InventoryTotal("ToyMart"); // Assert Assert.Equal(0m, result); }
The first unit test is to check to see if the numbers add up correctly. You might want to add tests to check edge case computations. I also added a unit test for an empty store. This is a situation where a store has no inventory and I wanted to make sure that an empty query result didn’t blow up the method. The second purpose of this unit test is to make other developers aware that any changes that they perform to the inventory computations don’t blow up when the store is empty. Always remember that unit tests protect you from your own programming mistakes but they also protect you from other developers who might not know what assumptions you made when you designed and built this method.
The Store Class
Now it’s time to test a method that is data destructive. I decided that there should be a class that is used by an administrator to add, edit and delete stores in the inventory. The basic CRUD operations. For my sample, I’m only going to implement the store delete function:
public class StoreMaintenance { private readonly IStoreAppContext _storeAppContext; public StoreMaintenance(IStoreAppContext storeAppContext) { _storeAppContext = storeAppContext; } public void DeleteStore(string storeName) { var storeList = (from s in _storeAppContext.Stores where s.Name == storeName select s).FirstOrDefault(); if (storeList != null) { _storeAppContext.Stores.Remove(storeList); _storeAppContext.SaveChanges(); } } }
There was one problem that I ran into when I tried to use the SaveChanges() method. The problem occurred becuase _storeAppContext is an interface and not an object itself. So the SaveChanges() method did not exist in the interface itself. Is I had to add it to the interface. Then I needed to implement the SaveChanges() method in the StoreAppContext object by calling the base class version inside the method. That creates an issue because my method is named the same as the method in the DBContext class and therefore it hides the base method. So I had to add a “new” keyword to notify the compiler that I was overriding the method. Here’s the final context object:
public class StoreAppContext : DbContext, IStoreAppContext { public StoreAppContext(DbContextOptions options) : base(options) { } public DbSet Products { get; set; } public DbSet Stores { get; set; } public new void SaveChanges() { base.SaveChanges(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.AddProduct("dbo"); modelBuilder.AddStore("dbo"); } }
Here is the final interface to match:
public interface IStoreAppContext : IDisposable { DbSet Products { get; set; } DbSet Stores { get; set; } void SaveChanges(); }
When deleting a store, I want to make sure all the product child records are deleted with it. In the database, I can setup a foreign constraint between the product and store tables and then make sure the cascade delete is set on. In the model builder for the product, I need to make sure that the foreign key is defined and it is also setup as a cascade delete:
public static class ProductConfig { public static void AddProduct(this ModelBuilder modelBuilder, string schema) { modelBuilder.Entity(entity => { entity.ToTable("Product", schema); entity.Property(e => e.Name).HasColumnType("varchar(50)"); entity.Property(e => e.Price).HasColumnType("money"); entity.Property(e => e.Quantity).HasColumnType("int"); entity.HasOne(d => d.StoreNavigation) .WithMany(p => p.Product) .HasForeignKey(d => d.Store) .OnDelete(DeleteBehavior.Cascade) .HasConstraintName("FK_store_product"); }); } }
You can see in the code above that the OnDelete method is setup as a cascade. Keep in mind that your database must be configured with the same foreign key constraint. If you fail to set it up in the database, you’ll get a passing unit test and your program will fail during run-time.
I’m now going to show the unit test for deleting a store:
[Fact] public void InventoryDeleteStoreViolationTest() { // Arrange ResetRecords(); var storeMaintenance = new StoreMaintenance(_storeAppContext); // Act storeMaintenance.DeleteStore("J-Foods"); // Assert Assert.Empty(from s in _storeAppContext.Stores where s.Name == "J-Foods" select s); // test for empty product list var productResults = from p in _storeAppContext.Products join s in _storeAppContext.Stores on p.Store equals s.Id where s.Name == "J-Foods" select p; Assert.Empty(productResults); }
As you can see, I performed two asserts in one unit test. Both unit tests are verifying that the one operation was correct. It’s not wise to perform too many asserts in a unit test because the difficulty to troubleshoot a failing unit test increases. Some instances, it doesn’t make sense to break the unit test into multiple tests. It’s OK to write two unit tests and test the removal of the store record independently from the removal of the product records. What I’m trying to describe here is: Don’t let this example of two asserts give you license to pack as many asserts as you can into a unit test. Keep unit tests as small and simple as possible. It’s preferred to have a larger quantity of tiny unit tests over a small quantity of large and complicated unit tests.
If you analyze the InventoryDeleteStoreViolationTest closely, you’ll realize that it is really testing three things: Does the cascade work, did the store record get deleted and did the child records get deleted. I would suggest you go back to the OnDelete method in the model builder for the product table and remove it (the entire OnDelete method line). Then run your unit tests and see what you get.
There is also a test in the DeleteStore method that tests for a search result of null. This is just in case someone tries to delete a store that was deleted. This should also have a unit test (I’ll leave that up to the reader to create one). For a normal CRUD design, you’ll list the stores and then you’ll probably put a delete button (or trash can icon) next to each store name. Then the administrator clicks the delete button and a pop-up confirms that the store will be deleted if they continue. The administrator continues, and you’re thinking “how could the query to get the store ever come up empty?” If only one person had access to that screen and there was no other place in the user interface that allowed the deletion of stores… Plus, nobody had access to the database back-end and couldn’t possibly delete the record by hand… you might think it was safe to assume a record will exist before deleting it. However, any large system with multiple administrators and other unforeseen activities going on in parallel can culminate in the possibility that the store record exists when the screen is first rendered, but becomes deleted just before the delete operation starts. Protect yourself. Never assume the record will be there.
Also, you are programming for how the code will work now. Several years down the road, as the program grows, another programmer could add a feature to the system allowing a different method of deleting a store. Suddenly, this screen, which has been “bug free” for years is throwing exceptions. My rule of thumb is this: If it can be null, it will be null!
One final thing to note: I threw all the unit tests in one unit test class. There are two business classes under unit tests. There should be two unit test classes, one for each business class to keep everything clean and simple. Unit test methods inside a class don’t execute in the order that they are listed. Don’t be surprised if the last unit test method executes before the first one. That means that you have to keep in mind that each unit test must be designed to be independent of other unit tests.
Where to Get the Code
You can go to my GitHub account and download the source by clicking here. I would recommend you download and add a few unit tests of your own to get familiar with the in-memory unit test database feature.