We will create an XUnit fixture to store our code that prepares and creates the fake DbContext. The class will use FakeItEasy to create a fake DbContext and uses reflection to replace the properties that are of the type IDbSet<T> with an instance of the TestDbSet<T> class we created which contains the inner list.

EF Core (previously named Entity Framework 7.0) InMemory data store

The re-written entity framework bits used in dot net core, provide a useful alternative to the above code called InMemory data store. This allows you to run your existing code against a data store that temporarily persists in memory for testing. Which means it requires less of the above boilerplate code to get started.

One downside is that to get a 'clean database' for each unit test some time-consuming setup tasks needs to be executed to get a clean state. When building read-only unit tests, this might not be that big of a deal, because the data store could be re-used. I am not sure if this gives the clearest tests because your database would also contain data to resolve other unit test scenarios, so you end up with a big setup method adding a lot of data for every case.

The upside is that it allows you to use entity framework queries like you would in production and perform actions as you would do in production. Actions could probably still be mocked to only test for delegation of actions, instead of testing if entity framework does its job.

A more Domain-driven design like approach

When using domain-driven design, we would try to get our model very close to how our domain experts talk about the different 'things' in the bounded context. Which makes it easy to introduce the specification pattern, because that would be something the domain expert specifies to define a restriction or query.

For example:

I would like to give all the customers from the Netherlands a discount code to get a 10% discount on their next purchase when I press this button.

or

I would like to get a report every month of customers from the Netherlands that used a discount code to purchase goods.

or

Only customers from the Netherlands are allowed to submit an order with a discount code.

An abstract implementation of this interface that implements the more generic IsSatisfiedBy part of the interface. One note: you might want to cache the compiled of the lambda expression for performance benefits if it is often executed.

Because the logic of the query is now moved to a specification class, we are required to change our repository implementation a bit. (In the above cases, one could also create a more generic repository class that accepts a lambda, but that is still code that is hard to test without preparing the data store).

You might expect the Save method on the repository to perform batch operations, without performing a lot of calls to the data store. A batch operation is a unit of work because it's more than one thing you do. I've separated this to the following interface and use an abstract implementation that won't call Save from the Add and Remove calls, but only performs this action on calling Commit:

This is probably the cleanest way of testing only the condition (query) that we own because there is not logic other than just the definition of the condition. Unit tests to check if the add, remove, or update actions are delegated can still be done with mocking like in the first scenario.

Specifications can also be used to filter or apply restrictions to non-entity framework data, which would decrease duplicated code.

If we look back to the rules, this is probably the best options to match them:

don't test code you don't own

we only need to test the specification condition.

make a test run as fast as possible

it's only one condition to test, which performs faster than creating a list and filling it.

unit test code should be easy (and quick) to understand

you now only need to know the condition, not look at what is currently in the data store and what is not in there.

One downside is that every query will result in a new class/ file on disk, which might not be that manageable if you stick them all in one folder. But you could also say it gives you a better view of the complexity of your application query logic because it is not hidden in business logic code or a large repository class with methods to query data.

Performance differences

As we expected the performance of the specification is the fastest, because it only needs to test one condition. The EF6 Fake is in the second place, and the EFCore InMemory is the slowest. This is mostly due to the creation of a clean data store each time. For read-only scenario's it will perform better.

Conclusion

EF6Core InMemory looks bad in the performance comparison, but if you would own around 1000 unit tests, it would still only take around 6 to 7 seconds to complete all of them. Once you re-use the data store context, it would perform a lot faster but might give a bit less clear tests in some cases. One downside is that it would require the existing code base to move to EFCore, which might be a big step to take.

The EF6 DbContext Fake is probably the easiest way to start unit testing your existing code base. Performs well and won't allow a lot of additional modifications to your code (If it currently is injected with a DbContext).

The DDD way requires a new way of thinking. The transformation of an existing code base might be a lot harder. In a recent code base, it will make unit testing the query conditions very smooth and bright, and fast. The result will probably be cleaner, more understandable code.