-
Notifications
You must be signed in to change notification settings - Fork 56
Rules for tests using EF Core
There are some rules to follow when when accessing a database via EF Core in xUnit test. This page explains each rule and how EF Core or EfCore.TestSupport can allow you to follow the five rules.
By default, xUnit will run each class containing tests in parallel, and every test method in a test class is then run in series. That means if you used the same database for all of your test, then many tests will be accessing / changing the database at the same time, which will make it impossible to check that the test works! There are two solutions:
- Use an in-memory database,
SqliteInMemory.CreateOptions<TContext>
- see Using SQLite in-memory databases - Use one of EfCore.TestSupport's methods that returns a database name that contains the class type name on the end. This means the test class has database that is unique to its test class. See
- SQL Server see Using SQL Server databases
- PostgreSQL see Using PostgreSQL databases
- Cosmos DB see Using Cosmos DB databases
NOTE: The SQL Server and PostgreSQL relies on a base connection string placed in an appsettings.json file in your xUnit test project - see Creating connection strings for more information.
2. You need an empty database at the start of each test AND 3, the database schema matches the current EF Core Model
I have combined rules 2 and 3 because they are fixed by the same EfCore.TestSupport features, but here are the reasons:
- Rule 2 is there because it much harder to write tests that don't care what the database contains. EF Core and EfCore.TestSupport provides ways to clear out all the rows in your tables (and more).
- Rule 3 is there because if you change any of your entity classes or the EF Core configuration, then the current database schema (e.g. tables, views, function etc) will be out of date and most likely not work with your new . EF Core and EfCore.TestSupport provides wasy to delete the current schema and recreated the schema using EF Core's current Model of the database.
There are two ways to implement rule 2 and 3:
- Call EF Core
EnsuredDeleted
method and then callEnsuredCreated
- see the testTestEnsureDeletedEnsureCreatedOk
in the TestSqlServerHelpers test class. - For SQL Server and PostgreSQL the
EnsuredDeleted
+EnsuredCreated
approach is a bit slow. Therefore EfCore.TestSupport has a method calledEnsureClean
which is quicker - see the testTestSqlDatabaseEnsureCleanOk
in the TestSqlServerHelpers test class.
NOTE: SqliteInMemory databases are empty by default and EnsuredCreated
will set up the correct schema.
An automatic test is usually broken down into three stages:
- SETUP (also known as Arrange) where you set up the environment for your test.
- ATTEMPT (also known as Act) where you execute some code that you want to test.
- VERIFY (also known as Assert) where you check the results for the code you tested are as you expected to be
To "set up the environment for your test" you often add data into the database in the SETUP stage. My experience is that setting up the database with the right data for a test can quite complex, in fact its often the hardest part of the test to write! Here is how I manage the seeding:
I normally start by putting the database seed code directly in the SETUP stage, but as soon as I find myself wanting copy that setup code I turn that code into an extension method in your Test project, with a good name. For instance I have a method called SeedDatabaseFourBooks()
that adds four books, with related data, into the database - see the the last method in the EfTestData static class I used when writing the 2nd edition of my book.
The other benefit to creating extension seeding methods is its a) easier to use, b) makes the test easier to understand, and c) easily to refactor/update the extension method as the application grows.
The last rule defined the three stages SETUP, ATTEMPT, and VERIFY, and this rule says that each stage should be isolated from each other to correctly mimic what would happen in your application. That isn't as simple as you might think because of a feature of EF Core.
The problem is that EF Core will automatically track all the entities that were read or written to the database across all the three stages which can hide errors in the database code you are testing. For instance you might write some data into the database in the SETUP, but that tracked data can cover over problems in your ATTEMPT, e.g. in SETUP you write a Book
class with collection of Review
classes. In the ATTEMPT stage you that Book
and you forget the .Include(book => book.Reviews)
. Because of the tracked entities the Book
will have the Reviews
collection filled, when in real use the Reviews
collection wouldn't be filled.
There are a few ways to fix this, but adding the following line of code context.ChangeTracker.Clear()
at the end of the SETUP and ATTEMPT stage will stop this issue. The code below shows this in action.
[Fact]
public void TestBookDbContextAddFourBooksLoadRelationshipsOk()
{
//SETUP
var options = SqliteInMemory.CreateOptions<BookDbContext>();
using var context = new BookDbContext(options);
context.Database.EnsureCreated();
context.SeedDatabaseFourBooks();
context.ChangeTracker.Clear();
//ATTEMPT
var book = context.Books.First();
book.Price = 123;
context.SaveChanges();
//VERIFY
context.ChangeTracker.Clear();
var verifyBook = context.Books.First();
verifyBook.Price.ShouldEqual(123);
}
NOTE: See this section in the "Changes in EfCore.TestSupport 5" article for a deeper explanation.
You need a way to provide the database options to your DbContext. This is done via a constructor in your DbContext which has a DbContextOptions<YourDbContext> options
parameter, which is normal for ASP.NET Core - see this example.
If you are using the OnConfiguring
approach you need add if (!optionsBuilder.IsConfigured)
into your OnConfiguring
method - see this example DbContext which has both a constructor with a DbContextOptions<YourDbContext> options
parameter AND an OnConfiguring
method.
- Testing against a PostgreSQL db
- Changes in EfCore.TestSupport 5
- Testing with production data
- Using an in-memory database (old)