Skip to main content

๐Ÿงช Unit Testing a Generic Repository in .NET with xUnit + Moq

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:

  1. ✅ Testing the repository with EF Core InMemory provider (integration-style)
  2. ✅ Testing service logic with Moq (mocking IGenericRepository and IUnitOfWork)

๐Ÿ“ฆ 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 Verify in Moq to ensure methods were called.
  • Keep controllers thin — test business logic in services.


Comments

Popular posts from this blog

๐Ÿ—️ Deep Dive: Understanding Every Concept in Microsoft Entra API Onboarding for .NET Developers

When working with Microsoft Entra (formerly Azure Active Directory), you’ll hear terms like App Registration, Tenant, Client ID, Audience, Scopes, Roles, Tokens, OBO flow , and more. If you’re new, it can feel overwhelming. This guide breaks down every key term and concept , with definitions, examples, and how they connect when you onboard and consume a new API. ๐Ÿ”น 1. Tenant Definition : A tenant in Entra ID is your organization’s dedicated, isolated instance of Microsoft Entra. Think of it like : Your company’s identity directory. Example : contoso.onmicrosoft.com is a tenant for Contoso Ltd. ๐Ÿ”น 2. App Registration Definition : The process of registering an application in Entra to give it an identity and permission to use Microsoft identity platform. Why needed : Without registration, Entra doesn’t know about your app. What it creates : Application (Client) ID – unique identifier for your app Directory (Tenant) ID – your organization’s ID Types of apps : Web ...

๐Ÿ—‘️ Garbage Collection & Resource Management in .NET (C#) — Beginner Friendly Guide

When you start working with .NET and C#, one of the biggest advantages is that you don’t need to manually manage memory like in C or C++. The Garbage Collector (GC) does most of the work for you. But here’s the catch — not everything is managed automatically. Some resources like files, database connections, sockets, and native memory still need special handling. This blog will help you understand: ✔ How the GC works ✔ What are managed vs unmanaged resources ✔ The difference between Dispose , Finalize , and using ✔ The Dispose pattern with examples ✔ Best practices every C# developer should know 1) How Garbage Collection Works in .NET Managed resources → Normal .NET objects (string, List, etc.). GC frees them automatically. Unmanaged resources → External resources like file handles, database connections, sockets, native memory. GC cannot clean them up — you must do it. ๐Ÿ‘‰ GC uses a Generational Model for performance: Gen 0 : Short-lived objects (local variables, t...

☁️ Azure Key vault Short Notes

๐ŸŸข What is Azure Key Vault? A cloud service for securely storing and accessing secrets, keys, and certificates . Removes the need to keep secrets (like connection strings, passwords, API keys) inside code or config files. Provides centralized secret management, encryption, and access control . ๐Ÿ‘‰ Think of it like a secure password manager but for your applications. ๐ŸŸข Key Features Secrets → store text values (e.g., DB connection string, API key). Keys → store cryptographic keys (RSA, EC) for encryption, signing. Certificates → store/manage SSL/TLS certificates. Access Control → Access Policies (older model). Azure RBAC (modern, preferred). Integration → works with App Service, Functions, AKS, VMs, SQL DB, etc. Logging → audit who accessed secrets via Azure Monitor / Diagnostic Logs. ๐ŸŸข Why Use Key Vault? Security → secrets are encrypted with HSM (Hardware Security Modules). Compliance → meet industry standards (PCI-DSS, ISO, GDPR). Automation → aut...