No to Moq
Merwan Chinta · Follow
Published in · 4 min read · Jan 16, 2024
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.
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