EF Core: In-Memory Database Unit testing (2024)

No to Moq

For unit testing dotnet applications that utilizes Entity Framework Core (EF Core), there’s an intriguing approach that blends the use of an in-memory database without the Moq framework. Let’s explore this by using an example: a CartManager and CartRepository.

Approach without In-Memory Database

When unit testing a manager class like CartManager, which depends on CartRepository, we often mock the repository. This method uses Moq to simulate the repository's behavior. However, this approach can sometimes lead to extensive setup for mocks, especially for complex queries.

Approach with In-Memory Database

An alternative is to use EF Core’s in-memory database. This approach involves actual database operations but in a lightweight, in-memory database. It provides a more realistic test environment compared to mocking.

EF Core: In-Memory Database Unit testing (3)

You’ll need to add the Microsoft.EntityFrameworkCore.InMemory package to your .NET Core unit test project for utilizing in-memory database.

// Package Manager Console
Install-Package Microsoft.EntityFrameworkCore.InMemory

We will setup the following:

  • CartDbContext for database context.
  • Startup class for configuring services and DI.
  • CartManager for business logic.
  • CartRepository for data access.
  • CartItem model representing an item in the cart.

This setup is modular, maintainable, and testable, aligning well with best practices in .NET Core development.

Cart Database Context Class

This is the EF Core DbContext, connecting your models to the database.

using Microsoft.EntityFrameworkCore;

public class CartDbContext : DbContext
{
public DbSet<CartItem> CartItems { get; set; }

public CartDbContext(DbContextOptions<CartDbContext> options)
: base(options)
{
}

// Other DB sets and configurations...
}

Configure Services Startup Class

In this code, we are injecting CartDbContext and specifying SQL Server as the database provider. We also register CartRepository and CartManager with scoped lifetimes, meaning a new instance is created per client request.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
public IConfiguration Configuration { get; }

public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public void ConfigureServices(IServiceCollection services)
{
// Configure EF Core with SQL Server (or any other provider)
services.AddDbContext<CartDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(
"DefaultConnection")));

// Register CartRepository and CartManager for dependency injection
services.AddScoped<CartRepository>();
services.AddScoped<CartManager>();
}
}

Cart Manager Class

The CartManager class contains business logic and interacts with the CartRepository.

public class CartManager
{
private readonly CartRepository _repository;

public CartManager(CartRepository repository)
{
_repository = repository;
}

public void AddItemToCart(CartItem item)
{
_repository.AddItem(item);
}

// Other business logic methods...
}

Cart Repository Class

The CartRepository class handles the data access logic.

public class CartRepository
{
private readonly CartDbContext _context;

public CartRepository(CartDbContext context)
{
_context = context;
}

public void AddItem(CartItem item)
{
_context.CartItems.Add(item);
_context.SaveChanges();
}

// Other data access methods...
}

Cart Item Model

This is a simple model representing an item in the cart.

public class CartItem
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }

// Other properties...
}

In this application code snippets, CartManager receives a request to add an item, it then delegates the action to CartRepository, which then adds the item to the CartDbContext and persists it to the SQL database.

For unit testing, we don’t want to use the real database. Instead, we switch to an in-memory database. We’ll set this up in our test project.

public class CartManagerTests
{
private ServiceProvider _serviceProvider;

[TestInitialize]
public void Setup()
{
var services = new ServiceCollection();

// Using In-Memory database for testing
services.AddDbContext<CartDbContext>(options =>
options.UseInMemoryDatabase("TestDb"));

services.AddScoped<CartRepository>();
services.AddScoped<CartManager>();

_serviceProvider = services.BuildServiceProvider();
}

In this setup, we’re building a new DI container specifically for our tests. We replace the SQL Server provider with the in-memory database.

You can also abstract part of above code (like AddScoped usages) into a common method referred by both actual application and unit tests logic, so that way all services configuration can be at centralized location.

Writing a Test

Let’s write a test using this setup.

[TestMethod]
public void AddItemToCart_Should_Add_Item()
{
// Arrange
using (var scope = _serviceProvider.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var manager = scopedServices.GetRequiredService<CartManager>();
var dbContext = scopedServices.GetRequiredService<CartDbContext>();

var item = new CartItem(/*parameters*/);

// Act
manager.AddItemToCart(item);

// Assert
var addedItem = dbContext.CartItems.Find(item.Id);
Assert.IsNotNull(addedItem);
Assert.AreEqual(item.Name, addedItem.Name);
}
}

In this test, we followed Arrange, Act and Assert pattern:

Arrange: Create a scope from our service provider, then resolve CartManager and CartDbContext. This CartManager instance will use the in-memory database context.

Act: Call the method under test AddItemToCart.

Assert: Check if the item was correctly added to the in-memory database.

Cleanup

Finally, ensure proper cleanup after each test to avoid data leakage between tests.

[TestCleanup]
public void Cleanup()
{
var dbContext = _serviceProvider.GetService<CartDbContext>();

dbContext.Database.EnsureDeleted();
}

This approach with in-memory database, ensures that:

  • The real application code remains unaffected and continues to use the actual database.
  • The unit tests run against an isolated in-memory database, providing a fast and reliable testing environment. In-memory database operations mimic real database interactions more closely than mocks.
  • Dependency injection maintains loose coupling between components, improving code maintainability and testability.
  • Less complex setup compared to configuring detailed mocks for every repository method.
  • More accurate testing of LINQ queries as they execute against a database structure.

I trust this information has been valuable to you. 🌟 Wishing you an enjoyable and enriching learning journey!

📚 For more insights like these, feel free to follow 👉 Merwan Chinta

EF Core: In-Memory Database Unit testing (2024)
Top Articles
Latest Posts
Article information

Author: Annamae Dooley

Last Updated:

Views: 6014

Rating: 4.4 / 5 (65 voted)

Reviews: 80% of readers found this page helpful

Author information

Name: Annamae Dooley

Birthday: 2001-07-26

Address: 9687 Tambra Meadow, Bradleyhaven, TN 53219

Phone: +9316045904039

Job: Future Coordinator

Hobby: Archery, Couponing, Poi, Kite flying, Knitting, Rappelling, Baseball

Introduction: My name is Annamae Dooley, I am a witty, quaint, lovely, clever, rich, sparkling, powerful person who loves writing and wants to share my knowledge and understanding with you.