The Generic Repository Pattern is common in .NET projects. It abstracts away EF Core details and makes your code testable. But how do you actually test it?
In this article, we’ll cover two approaches:
- ✅ Testing the repository with EF Core InMemory provider (integration-style)
- ✅ Testing service logic with Moq (mocking
IGenericRepositoryandIUnitOfWork)
๐ฆ Setup — Test Project Dependencies
Add these NuGet packages to your test project:
dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package FluentAssertions
dotnet add package Moq
๐️ Step 1: Our Product Entity & DbContext
We’ll use a simple Product entity and a minimal DbContext:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
}
๐️ Step 2: Generic Repository Interface & Implementation
Interface:
public interface IGenericRepository<T> where T : class
{
Task<T> GetByIdAsync(object id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task AddAsync(T entity);
void Update(T entity);
void Remove(T entity);
IQueryable<T> Query();
}
Implementation (simplified):
public class GenericRepository<T> : IGenericRepository<T> where T : class
{
private readonly DbContext _context;
private readonly DbSet<T> _dbSet;
public GenericRepository(DbContext context)
{
_context = context;
_dbSet = _context.Set<T>();
}
public async Task<T> GetByIdAsync(object id) => await _dbSet.FindAsync(id);
public async Task<IEnumerable<T>> GetAllAsync() => await _dbSet.ToListAsync();
public async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate) =>
await _dbSet.Where(predicate).ToListAsync();
public async Task AddAsync(T entity) => await _dbSet.AddAsync(entity);
public void Update(T entity) => _context.Entry(entity).State = EntityState.Modified;
public void Remove(T entity) => _dbSet.Remove(entity);
public IQueryable<T> Query() => _dbSet.AsQueryable();
}
๐งช Step 3: xUnit Tests with EF Core InMemory
This is a realistic test since it uses EF Core’s InMemory provider (but still no SQL database).
public class GenericRepositoryTests
{
private static AppDbContext CreateDbContext(string dbName = null)
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(dbName ?? Guid.NewGuid().ToString())
.Options;
return new AppDbContext(options);
}
private static GenericRepository<Product> CreateRepository(AppDbContext ctx) =>
new GenericRepository<Product>(ctx);
[Fact]
public async Task AddAsync_Should_Add_Product()
{
using var ctx = CreateDbContext();
var repo = CreateRepository(ctx);
var product = new Product { Name = "Test", Price = 10m };
await repo.AddAsync(product);
await ctx.SaveChangesAsync();
var saved = await ctx.Products.FirstAsync();
saved.Name.Should().Be("Test");
}
[Fact]
public async Task GetByIdAsync_Should_Return_Product()
{
using var ctx = CreateDbContext();
ctx.Products.Add(new Product { Id = 1, Name = "P1", Price = 5m });
await ctx.SaveChangesAsync();
var repo = CreateRepository(ctx);
var product = await repo.GetByIdAsync(1);
product.Should().NotBeNull();
product.Name.Should().Be("P1");
}
[Fact]
public async Task Update_Should_Modify_Product()
{
using var ctx = CreateDbContext();
var product = new Product { Name = "Old", Price = 5m };
ctx.Products.Add(product);
await ctx.SaveChangesAsync();
var repo = CreateRepository(ctx);
product.Name = "Updated";
repo.Update(product);
await ctx.SaveChangesAsync();
var updated = await ctx.Products.FindAsync(product.Id);
updated.Name.Should().Be("Updated");
}
[Fact]
public async Task Remove_Should_Delete_Product()
{
using var ctx = CreateDbContext();
var product = new Product { Name = "DeleteMe", Price = 3m };
ctx.Products.Add(product);
await ctx.SaveChangesAsync();
var repo = CreateRepository(ctx);
repo.Remove(product);
await ctx.SaveChangesAsync();
var exists = await ctx.Products.AnyAsync(p => p.Id == product.Id);
exists.Should().BeFalse();
}
}
✅ These tests verify the repository actually works with EF Core InMemory.
๐งช Step 4: Moq Tests — Service Layer Without EF
Sometimes you don’t want EF at all — you only want to check if your service calls the repository correctly. That’s where Moq shines.
Example 1: Mocking IGenericRepository<Product>
public class ProductService
{
private readonly IGenericRepository<Product> _repo;
public ProductService(IGenericRepository<Product> repo) => _repo = repo;
public async Task<IEnumerable<Product>> GetAllProductsAsync() => await _repo.GetAllAsync();
public async Task AddProductAsync(string name, decimal price) =>
await _repo.AddAsync(new Product { Name = name, Price = price });
}
public class ProductServiceWithRepoTests
{
[Fact]
public async Task GetAllProductsAsync_Returns_All_Products()
{
var products = new List<Product>
{
new Product { Id = 1, Name = "P1", Price = 10m },
new Product { Id = 2, Name = "P2", Price = 20m }
};
var mockRepo = new Mock<IGenericRepository<Product>>();
mockRepo.Setup(r => r.GetAllAsync()).ReturnsAsync(products);
var service = new ProductService(mockRepo.Object);
var result = await service.GetAllProductsAsync();
result.Should().HaveCount(2);
mockRepo.Verify(r => r.GetAllAsync(), Times.Once);
}
}
Example 2: Mocking IUnitOfWork
If your service uses a Unit of Work to commit transactions, you can mock that too:
public class ProductServiceUsingUow
{
private readonly IUnitOfWork _uow;
public ProductServiceUsingUow(IUnitOfWork uow) => _uow = uow;
public async Task CreateProductAsync(string name, decimal price)
{
var repo = _uow.Repository<Product>();
await repo.AddAsync(new Product { Name = name, Price = price });
await _uow.CommitAsync();
}
}
public class ProductServiceWithUowTests
{
[Fact]
public async Task CreateProductAsync_Calls_Repo_And_Commits()
{
var mockRepo = new Mock<IGenericRepository<Product>>();
var mockUow = new Mock<IUnitOfWork>();
mockUow.Setup(u => u.Repository<Product>()).Returns(mockRepo.Object);
mockUow.Setup(u => u.CommitAsync()).Returns(Task.CompletedTask);
var service = new ProductServiceUsingUow(mockUow.Object);
await service.CreateProductAsync("Atomic", 50m);
mockRepo.Verify(r => r.AddAsync(It.Is<Product>(p => p.Name == "Atomic" && p.Price == 50m)), Times.Once);
mockUow.Verify(u => u.CommitAsync(), Times.Once);
}
}
✅ Key Takeaways
- xUnit + EF InMemory → good for integration-style tests of repository logic.
- Moq → great for unit testing services without touching EF.
- Use
Verifyin Moq to ensure methods were called. - Keep controllers thin — test business logic in services.
Comments
Post a Comment