Comprehensive Testing in .NET 8: Using Moq and In-Memory Databases

extinctsion

Aditya Sharma

Posted on November 12, 2024

Comprehensive Testing in .NET 8: Using Moq and In-Memory Databases

Introduction

Testing is a core part of developing reliable applications, and with .NET 8, testing has become even more flexible and powerful.
In this article, I'll walk you through how to set up and utilize the Moq library and an in-memory database for comprehensive testing in .NET 8. We'll cover unit testing, integration testing, and some tips on best practices.

Unit Test

Prerequisites

Before we begin, make sure you have:

  • NET 8 SDK
  • Moq library (for mocking dependencies)
  • Microsoft.EntityFrameworkCore.InMemory (for in-memory database support)

Why Moq and In-Memory Databases?

  • Moq is an essential library for mocking interfaces in unit tests, allowing you to test your code's behavior without relying on external dependencies.
  • In-memory databases are useful for integration tests, letting you test your repository or service layer without setting up an actual database.

Setting Up Your Project

Create a new .NET 8 project, if you haven't already:

dotnet new webapi -n TestingDemo
cd TestingDemo
dotnet add package Moq
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Enter fullscreen mode Exit fullscreen mode

Step 1: Define the Product Model and Database Context

Start by defining a Product class and a corresponding AppDbContext for Entity Framework Core:

// Models/Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// Data/AppDbContext.cs
using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Product> Products { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Create the Repository Interface and Implementation

Define a repository interface and its implementation to manage products. This way, we can mock this interface later in tests.

// Repositories/IProductRepository.cs
public interface IProductRepository
{
    Task<IEnumerable<Product>> GetAllAsync();
    Task<Product> GetByIdAsync(int id);
    Task AddAsync(Product product);
}

// Repositories/ProductRepository.cs
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;

    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<IEnumerable<Product>> GetAllAsync() => await _context.Products.ToListAsync();

    public async Task<Product> GetByIdAsync(int id) => await _context.Products.FindAsync(id);

    public async Task AddAsync(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Write Unit Tests with Moq

We'll use Moq to create a unit test for ProductService. This service uses IProductRepository, so we can mock it to control its behavior during testing.

// Tests/ProductServiceTests.cs
using Moq;
using Xunit;

public class ProductServiceTests
{
    private readonly Mock<IProductRepository> _mockRepo;
    private readonly ProductService _productService;

    public ProductServiceTests()
    {
        _mockRepo = new Mock<IProductRepository>();
        _productService = new ProductService(_mockRepo.Object);
    }

    [Fact]
    public async Task GetAllProducts_ReturnsAllProducts()
    {
        // Arrange
        var products = new List<Product>
        {
            new Product { Id = 1, Name = "Product1", Price = 10 },
            new Product { Id = 2, Name = "Product2", Price = 20 },
        };
        _mockRepo.Setup(repo => repo.GetAllAsync()).ReturnsAsync(products);

        // Act
        var result = await _productService.GetAllProducts();

        // Assert
        Assert.Equal(2, result.Count());
        Assert.Equal("Product1", result.First().Name);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this test:

  1. We mock IProductRepository using Moq.
  2. We configure the GetAllAsync method to return a predefined list of products.
  3. We validate that GetAllProducts returns the expected number of products and values.

Step 4: Integration Testing with In-Memory Database

Integration tests help verify that all parts of your code work together. Here, we’ll use an in-memory database to test ProductRepository without needing a real database.

// Tests/ProductRepositoryTests.cs
using Microsoft.EntityFrameworkCore;
using Xunit;

public class ProductRepositoryTests
{
    private AppDbContext GetInMemoryDbContext()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase(databaseName: "TestDatabase")
            .Options;
        return new AppDbContext(options);
    }

    [Fact]
    public async Task AddProduct_SavesProductToDatabase()
    {
        // Arrange
        var context = GetInMemoryDbContext();
        var repository = new ProductRepository(context);
        var product = new Product { Name = "Test Product", Price = 15.5m };

        // Act
        await repository.AddAsync(product);
        var savedProduct = await context.Products.FirstOrDefaultAsync();

        // Assert
        Assert.NotNull(savedProduct);
        Assert.Equal("Test Product", savedProduct.Name);
    }
}
Enter fullscreen mode Exit fullscreen mode

In this test:

  1. We use UseInMemoryDatabase to create a temporary database in memory.
  2. We add a product and check that it was saved correctly.
  3. The Assert statements verify the data’s integrity.

Step 5: Testing Edge Cases and Best Practices

When testing, consider the following best practices:

  1. Cover Edge Cases: Always test edge cases, such as null values or large numbers.
  2. Isolation: Use Moq to isolate service dependencies in unit tests.
  3. Clean Up Resources: For integration tests, ensure resources like the in-memory database are properly disposed of.
  4. Use Assert.Throws for Exception Testing: When testing methods that throw exceptions, Assert.Throws helps verify that exceptions are correctly handled.

Example of Testing an Exception

[Fact]
public async Task GetById_ThrowsException_WhenIdNotFound()
{
    // Arrange
    _mockRepo.Setup(repo => repo.GetByIdAsync(It.IsAny<int>())).ReturnsAsync((Product)null);

    // Act & Assert
    await Assert.ThrowsAsync<KeyNotFoundException>(() => _productService.GetProductById(999));
}
Enter fullscreen mode Exit fullscreen mode

In this test, we simulate an exception scenario where GetByIdAsync returns null, expecting a KeyNotFoundException when calling GetProductById.

Conclusion

By combining Moq with in-memory databases, you can cover both unit and integration testing, ensuring your .NET 8 applications are robust and reliable. Mocking with Moq allows you to isolate components for precise unit tests, while in-memory databases enable realistic integration tests without complex database setups.

Testing in .NET 8 is not only powerful but also versatile, and using these tools together can help you achieve a higher standard of code quality. Happy testing!

Unit Test

💖 💪 🙅 🚩
extinctsion
Aditya Sharma

Posted on November 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related