-
Notifications
You must be signed in to change notification settings - Fork 56
Using SQLite in memory databases
SQLite has an in-memory mode that makes using SQLite in-memory databases easy to use. The positives are that an in-memory database obeys the 5 rules for EF Core tests plus some extra .
- Rule 1 - The database is unique, not only to the test class but to the test method.
- Rule 2 - The database is empty, which is a good starting point for a unit test.
- Rule 3 - Because you create a new database every time, it matches the current EF Core's Model.
- Your unit tests will run successfully in the Test part of a DevOps pipeline without needing a database.
- Your unit tests are faster.
You can use SQLite in-memory databases for unit testing your EF Core code, even if your production database is of a different type, say SQL Servers. BUT(!) if your production database isn't using a SQLite database, then you can have problems, mainly around SQL commands and some types such as decimal
. So, if in doubt, always test using the same database type as your production system uses.
NOTE: I give a lot more information on how to decide if you can use a SQLite in-memory database in chapter 17 of my book, Entity Framework Core in Action, second edition.
The SqliteInMemory.CreateOptions<T>
method will create options that will provide a SQLite, in-memory database for unit testing. The code below shows how is can be used to create .
[Fact]
public void TestSqliteOk()
{
//SETUP
var options = SqliteInMemory.CreateOptions<EfCoreContext>();
using var context = new EfCoreContext(options))
context.Database.EnsureCreated();
//... rest of unit test goes here
The SqliteInMemory.CreateOptions<MyDbContext>()
method returns a class that implements DbContextOptions<MyDbContext>
needed by the database, but now also implements IDisposable
. This is done to dispose SQLite's connection (which contains the in-memory data) when the context is disposed. (I didn't do that in the version 3.2.0, and I should have because the database should be disposed at the end.)
This means when the application DbContext using that option is disposed the Dispose
method in the options class is also called and the connection is disposed. This means if you create multiple contexts, then you need to do something extra to stop the the options
variable being disposed, otherwise the database will be lost to subsequent DbContext instances.
For example, the following code will fail.
public void TestSqliteTwoInstancesBAD()
{
//SETUP
var options = SqliteInMemory.CreateOptions<BookContext>();
using (var context = new BookContext(options))
{
context.Database.EnsureCreated();
context.SeedDatabaseFourBooks();
}
using (var context = new BookContext(options))
{
//ATTEMPT
//THIS WILL FAIL!!!! THIS WILL FAIL!!!! THIS WILL FAIL!!!!
var books = context.Books.ToList();
//VERIFY
books.Last().Reviews.ShouldBeNull();
}
}
There are three ways around this:
-
Best approach: Have one instance of the application DbContext and use
ChangeTracker.Clear()
. -
Keep your Using(var...: Have two instances of the application DbContext and call
options.StopNextDispose()
- Lots of DbContext instances: Turn off Dispose and manually dispose at the end
In EF Core 5 there is new feature triggered by context.ChangeTracker.Clear()
. This clears out all the tracked entities in the current instance of the application DbContext. This means you don't need multiple DbContexts to check that your second part worked property.
public void TestSqliteOneInstanceWithChangeTrackerClearOk()
{
//SETUP
var options = SqliteInMemory.CreateOptions<BookContext>();
using var context = new BookContext(options);
context.Database.EnsureCreated();
context.SeedDatabaseFourBooks();
context.ChangeTracker.Clear(); //NEW LINE ADDED
//ATTEMPT
var books = context.Books.ToList();
//VERIFY
books.Last().Reviews.ShouldBeNull();
}
NOTE: It also allows you to use using var context = ...
- see line 5. This makes the code simpler, and quicker to write. That's why I recommend this version
If you want to keep the two instances of the application DbContext, then you need to use the options.StopNextDispose()
to stop the dispose on the first application DbContext instance. You can call the StopNextDispose
method any time before the first application DbContext instance is disposed, but I tend to do it right under the creating of the option, as shown in line 5 of the code below
public void TestSqliteTwoInstancesGood()
{
//SETUP
var options = SqliteInMemory.CreateOptions<BookContext>();
options.StopNextDispose();
using (var context = new BookContext(options))
{
context.Database.EnsureCreated();
context.SeedDatabaseFourBooks();
}
using (var context = new BookContext(options))
{
//ATTEMPT
var books = context.Books.ToList();
//VERIFY
books.Last().Reviews.ShouldBeNull();
}
}
If you have multiple instances of the application DbContext, then you can use the options.TurnOffDispose()
and call the options.ManualDispose()
methods at the end of the unit test.
public void TestSqliteThreeInstancesOk()
{
//SETUP
var options = SqliteInMemory.CreateOptions<BookContext>();
options.TurnOffDispose();
using (var context = new BookContext(options))
{
context.Database.EnsureCreated();
context.SeedDatabaseFourBooks();
}
using (var context = new BookContext(options))
{
//ATTEMPT
var books = context.Books.ToList();
//VERIFY
books.Last().Reviews.ShouldBeNull();
}
using (var context = new BookContext(options))
{
//ATTEMPT
var books = context.Books.ToList();
//VERIFY
books.Last().Reviews.ShouldBeNull();
}
options.ManualDispose();
}
NOTE the call to options.TurnOnDispose();
before the last application DbContext.
The SQLite in-memory options extension methods have an optional parameter that allows you to set extra options at the DbContextOptionsBuilder<T>
level. Below is part of the unit tests showing how to add/override options.
//... previous code removed to focus on the feature
var options = SqliteInMemory.CreateOptions<BookContext>(
//sets a tracking behavior
builder => builder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
using (var context = new BookContext(options))
{
//VERIFY
var book = context.Books.First();
context.Entry(book).State.ShouldEqual(EntityState.Detached);
}
There are variations of these methods that include logging output - see Tools for capturing EF Core logging for more info.
- Testing against a PostgreSQL db
- Changes in EfCore.TestSupport 5
- Testing with production data
- Using an in-memory database (old)