ASP.NET Core  

Testing with Moq for Clean and Reliable Code in ASP.NET Core with C# 13 and xUnit

Overview

C# 13 and .NET 8 have greatly enhanced ASP.NET Core development capabilities. However, building scalable and maintainable systems requires robust testing in addition to feature implementation. Using xUnit, Moq, and the latest C# 13 features, you will learn how to write clean, reliable, and testable code.

This guide will walk you through testing a REST API or a service layer:

  • Creating a test project
  • Using xUnit to write clean unit tests
  • Using Moq to mock dependencies
  • Using best practices for test architecture and maintainability

 

Setting Up Your ASP.NET Core Project with C# 13

With ASP.NET Core Web API and C# 13, begin with .NET 8 and ASP.NET Core Web API.

dotnet new sln -n ZiggyRafiqApi

dotnet new web -n ZiggyRafiq.Api
dotnet new classlib -n ZiggyRafiq.Domain
dotnet new classlib -n ZiggyRafiq.Core
dotnet new classlib -n ZiggyRafiq.Application
dotnet new classlib -n ZiggyRafiq.Infrastructure
 

dotnet sln add ZiggyRafiq.Api/ZiggyRafiq.Api.csproj
dotnet sln add ZiggyRafiq.Domain/ZiggyRafiq.Domain.csproj
dotnet sln add ZiggyRafiq.Core/ZiggyRafiq.Core.csproj
dotnet sln add ZiggyRafiq.Application/ZiggyRafiq.Application.csproj
dotnet sln add ZiggyRafiq.Infrastructure/ZiggyRafiq.Infrastructure.csproj
 

Create a Test Project with xUnit and Moq

Add a new test project:

dotnet new xunit -n ZiggyRafiqApi.Tests
dotnet add ZiggyRafiqApi.Tests/ZiggyRafiqApi.Tests.csproj reference ZiggyRafiqApi/ZiggyRafiqApi.csproj
dotnet add ZiggyRafiqApi.Tests package Moq

Use Case: Testing a Service Layer

Domain, Service, Respository, Interface and API

namespace ZiggyRafiqApi.Domain;
public record Order(Guid Id, string Status);

using ZiggyRafiqApi.Domain;

namespace ZiggyRafiqApi.Core.Interfaces;

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
}

using ZiggyRafiqApi.Core.Interfaces;

namespace ZiggyRafiqApi.Application.Services;

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public async Task<string> GetOrderStatusAsync(Guid orderId)
    {
        var order = await _repository.GetByIdAsync(orderId);
        return order?.Status ?? "Not Found";
    }
}
using ZiggyRafiqApi.Core.Interfaces;
using ZiggyRafiqApi.Domain;

namespace ZiggyRafiq.Infrastructure.Repositories
{
    public class InMemoryOrderRepository : IOrderRepository
    {
        private readonly List<Order> _orders = new()
    {
        new Order(Guid.Parse("7c3308b4-637f-426b-aafc-471697dabeb4"), "Processed"),
        new Order(Guid.Parse("5aee5943-56d0-4634-9f6c-7772f6d9c161"), "Pending")
    };

        public Task<Order?> GetByIdAsync(Guid id)
        {
            var order = _orders.FirstOrDefault(o => o.Id == id);
            return Task.FromResult(order);
        }
    }
}

using ZiggyRafiq.Infrastructure.Repositories;
using ZiggyRafiqApi.Application.Services;
using ZiggyRafiqApi.Core.Interfaces;
 

var builder = WebApplication.CreateBuilder(args);


builder.Services.AddScoped<IOrderRepository, InMemoryOrderRepository>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

app.MapGet("/orders/{id:guid}", async (Guid id, OrderService service) =>
{
    var status = await service.GetOrderStatusAsync(id);
    return Results.Ok(new { OrderId = id, Status = status });
});

app.Run();

Unit Testing with xUnit and Moq

Test Class

using Moq;
using ZiggyRafiqApi.Application.Services;
using ZiggyRafiqApi.Core.Interfaces;
using ZiggyRafiqApi.Domain;


namespace OrderApi.Tests;

public class OrderServiceTests
{
    private readonly Mock<IOrderRepository> _mockRepo;
    private readonly OrderService _orderService;

    public OrderServiceTests()
    {
        _mockRepo = new Mock<IOrderRepository>();
        _orderService = new OrderService(_mockRepo.Object);
    }

    [Fact]
    public async Task GetOrderStatusAsync_ReturnsStatus_WhenOrderExists()
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync(new Order(orderId, "Processed"));

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal("Processed", result);
    }

    [Fact]
    public async Task GetOrderStatusAsync_ReturnsNotFound_WhenOrderDoesNotExist()
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync((Order?)null);

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal("Not Found", result);
    }

    [Theory]
    [InlineData("Processed")]
    [InlineData("Pending")]
    [InlineData("Shipped")]
    public async Task GetOrderStatus_ReturnsCorrectStatus(string status)
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync(new Order(orderId, status));

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal(status, result);
    }
}

Best Practices

1. Use Dependency Injection for Testability

All dependencies should be injected, so don't use static classes or service locator patterns.

2. Keep Tests Isolated

In order to isolate external behavior, Moq should be used to isolate database/network I/O from tests.

3. Use Theory for Parameterized Tests

    [Theory]
    [InlineData("Processed")]
    [InlineData("Pending")]
    [InlineData("Shipped")]
    public async Task GetOrderStatus_ReturnsCorrectStatus(string status)
    {
        var orderId = Guid.NewGuid();
        _mockRepo.Setup(r => r.GetByIdAsync(orderId))
                 .ReturnsAsync(new Order(orderId, status));

        var result = await _orderService.GetOrderStatusAsync(orderId);

        Assert.Equal(status, result);
    }

4. Group Tests by Behavior (Not CRUD)

Tests should be organized according to what systems do, not how they are performed. For example:

  • GetOrderStatus_ShouldReturnCorrectStatus
  • CreateOrder_ShouldSendNotification

5. Use Records for Test Data in C# 13

public record Order(Guid Id, string Status);

Immutable, concise, and readable test data objects can be created using records.

Test Coverage Tips

  • To measure test coverage, use Coverlet or JetBrains dotCover.
  • Business rules and logic at the service layer should be targeted.
  • Make sure you do not overtest third-party libraries or trivial getter/setter functions.

Recommended Tools

Tool

Purpose

xUnit

Unit Testing Framework

Moq

Mocking Dependencies

FluentAssertions

Readable Assertions

Coverlet

Code Coverage

Summary

Test ASP.NET Core applications with xUnit, Moq, and C# 13 features. Develop clean architecture, isolated unit tests, and meaningful test names to ensure that your applications are reliable. As a result of combining DI, mocking, and xUnit assertions, developers are able to identify and fix issues earlier in the development cycle, resulting in faster feedback, higher confidence, and more maintainable systems. The isolation of unit tests ensures that each component works independently, improving the system's reliability as a whole. A clean architecture and meaningful test names also improve the ease of understanding and maintaining a codebase over time. This approach reduces regressions and improves code quality, fostering a robust development process. Additionally, clear test cases and modular designs make it easier to onboard and debug team members, promoting collaboration among team members. In the end, these practices lead to applications that are more resilient and scalable.

You can access and download the source code for this article from Ziggy Rafiq Repository on GitHub.

 

Capgemini is a global leader in consulting, technology services, and digital transformation.