Easy InMemory Unit Tests with EFCore and Sqlite (2024)

Easy InMemory Unit Tests with EFCore and Sqlite (3)

Unit testing is a type of software testing that is performed on the smallest unit of a software application, called a “unit”. A unit can be a function, method or module that is tested in isolation from other parts of the code.

The purpose of unit testing is to ensure that a single unit of software functions correctly and produces the expected results. To do this, test cases are developed that cover various inputs and situations and check the expected outputs. Unit tests are usually created and executed automatically to make the development process more efficient and reliable.

However, there are different opinions whether database functionalities should be solved via unit tests: because database providers have certain peculiarities that can only be tested against a real database.
In principle, I agree — but there are simply certain scenarios — like Linq queries — that can be covered super easily and stably via unit tests. Of course, however, these do not replace integration tests against real databases, which, however, have to be implemented with higher effort, costs and not always corresponding benefits.

To implement certain scenarios with the help of unit tests in .NET, I like to use Sqlite with its InMemory features, which is very easy and stable to use. This in itself is also the recommended variant, although nothing is said about the InMemory functionality of Sqlite itself. It is related to the fact that the InMemory database driver of EFCore is no longer developed, has immense vulnerabilities and incompatibilities and should simply not be used.

The .NET implementation of Sqlite ( NuGet Package: Microsoft.EntityFrameworkCore.Sqlite) also supports the InMemory behavior; it just has a certain peculiarity: InMemory is not supported by a name as known from other databases, but by a connection instance. If the connection is closed, the InMemory database is discarded.

// create test im-memory connection
SqliteConnection sqliteConnection = new("DataSource=:memory:");

// ..
DbContextOptions<T> options = new DbContextOptionsBuilder<T>()
.UseSqlite(sqliteConnection) // re-use connection
.EnableSensitiveDataLogging(true)
.Options;

Therefore, the test case must share the connection across multiple DbContext instances, which I implemented accordingly via a test helper class.

To simplify the tests and their implementation I have written a small helper:

/// <summary>
/// Static class to handle methods related to Sqlite database testing
/// </summary>
public static class TestSqliteDatabase
{
/// <summary>
/// Method to create a Sqlite connection for testing purposes
/// </summary>
/// <returns>SqliteConnection object for testing</returns>
public static SqliteConnection CreateSqliteTestConnection()
{
SqliteConnection connection = new("DataSource=:memory:");
connection.Open();
return connection;
}

/// <summary>
/// Method to create options for the provided SqlServerBaseDbContext
/// </summary>
/// <typeparam name="T">Type of SqlServerBaseDbContext</typeparam>
/// <param name="sqliteConnection">SqliteConnection to provide options for</param>
/// <returns>DbContextOptions object with options for the provided SqlServerBaseDbContext</returns>
public static DbContextOptions<T> CreateOptions<T>(SqliteConnection sqliteConnection) where T : SqlServerBaseDbContext
=> new DbContextOptionsBuilder<T>()
.UseSqlite(sqliteConnection)
.EnableSensitiveDataLogging(true)
.Options;

/// <summary>
/// Method to create a DbContext for the provided SqlServerBaseDbContext and connection
/// </summary>
/// <typeparam name="T">Type of SqlServerBaseDbContext</typeparam>
/// <param name="sqliteConnection">SqliteConnection to provide context for</param>
/// <returns>New instance of the provided SqlServerBaseDbContext connected to the provided SqliteConnection</returns>
public static T CreateContext<T>(SqliteConnection sqliteConnection) where T : SqlServerBaseDbContext
{
DbContextOptions<T> dbContextOptions = CreateOptions<T>(sqliteConnection);

T context = (T)Activator.CreateInstance(typeof(T), dbContextOptions)!;
context.Database.EnsureCreated();

return context;
}
}

A test case always has the same structure with regard to the test of the database functionalities:

  • Establishment of the connection, which is shared
  • First context instance for seeding the database
  • Second context to test the actual functionality
[Fact]
public async Task My_Nice_Repository_Test_Case()
{
// test database context
using SqliteConnection testDbConnection = TestSqliteDatabase
.CreateSqliteTestConnection();

// arrange
// ...

// create test database instance for seed
using (TenantsDatabaseSqlServerContext dbContext = TestSqliteDatabase
.CreateContext<MyDatabaseMssqlServerContext>(testDbConnection))
{
// use this to seed your database

await dbContext.SaveChangesAsync(ct);
}

// create test database instance for query
using (TenantsDatabaseSqlServerContext dbContext = TestSqliteDatabase
.CreateContext<MyDatabaseMssqlServerContext>(testDbConnection))
{
// act
// ...
}
}

It is not necessarily the case that there is no third or fourth context; it is just essential to ensure that the Act part is given a new, fresh context and does not use the seed context, so that the test cannot be falsified.

Unit tests offer several benefits. They help developers detect and fix bugs early by isolating potential problems and preventing bugs from spreading to other parts of the application. Writing tests also more clearly defines requirements and unit behaviors, leading to better code quality and maintainability. Unit tests also facilitate refactoring by providing a safety net to ensure that changes do not have unwanted side effects.

Overall, unit tests are an important practice in software development to improve the quality and stability of an application. They help reduce bugs, increase developer productivity, and improve customer satisfaction.

My variant to implement EF Core unit tests is very simple, works very stable and adds value to all my projects.

Easy InMemory Unit Tests with EFCore and Sqlite (4)

Benjamin Abt

Ben is a passionate developer and software architect and especially focused on .NET, cloud and IoT. In his professional he works on high-scalable platforms for IoT and Industry 4.0 focused on the next generation of connected industry based on Azure and .NET. He runs the largest german-speaking C# forum myCSharp.de, is the founder of the Azure UserGroup Stuttgart, a co-organizer of the AzureSaturday, runs his blog, participates in open source projects, speaks at various conferences and user groups and also has a bit free time. He is a Microsoft MVP since 2015 for .NET and Azure.

Easy InMemory Unit Tests with EFCore and Sqlite (2024)

References

Top Articles
Latest Posts
Article information

Author: Madonna Wisozk

Last Updated:

Views: 5964

Rating: 4.8 / 5 (48 voted)

Reviews: 87% of readers found this page helpful

Author information

Name: Madonna Wisozk

Birthday: 2001-02-23

Address: 656 Gerhold Summit, Sidneyberg, FL 78179-2512

Phone: +6742282696652

Job: Customer Banking Liaison

Hobby: Flower arranging, Yo-yoing, Tai chi, Rowing, Macrame, Urban exploration, Knife making

Introduction: My name is Madonna Wisozk, I am a attractive, healthy, thoughtful, faithful, open, vivacious, zany person who loves writing and wants to share my knowledge and understanding with you.