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 HflApi
dotnet new web -n Hfl.Api
dotnet new classlib -n Hfl.Domain
dotnet new classlib -n Hfl.Core
dotnet new classlib -n Hfl.Application
dotnet new classlib -n Hfl.Infrastructure
dotnet sln add Hfl.Api/Hfl.Api.csproj
dotnet sln add Hfl.Domain/Hfl.Domain.csproj
dotnet sln add Hfl.Core/Hfl.Core.csproj
dotnet sln add Hfl.Application/Hfl.Application.csproj
dotnet sln add Hfl.Infrastructure/Hfl.Infrastructure.csproj
Create a Test Project with xUnit and Moq
Add a new test project:
dotnet new xunit -n HflApi.Tests
dotnet add HflApi.Tests/HflApi.Tests.csproj reference HflApi/HflApi.csproj
dotnet add HflApi.Tests package Moq
Use Case: Testing a Service Layer
Domain, Service, Respository, Interface and API
namespace HflApi.Domain;
public record Order(Guid Id, string Status);
using HflApi.Domain;
namespace HflApi.Core.Interfaces;
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
}
using HflApi.Core.Interfaces;
namespace HflApi.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 HflApi.Core.Interfaces;
using HflApi.Domain;
namespace Hfl.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 Hfl.Infrastructure.Repositories;
using HflApi.Application.Services;
using HflApi.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 HflApi.Application.Services;
using HflApi.Core.Interfaces;
using HflApi.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
Use xUnit, Moq, and C# 13 capabilities to test ASP.NET Core applications. To make sure your apps are dependable, provide a clean architecture, separated unit tests, and appropriate test names. Developers can find and address problems earlier in the development cycle by integrating DI, mocking, and xUnit assertions. This leads to quicker feedback, more confidence, and more maintainable systems. Unit tests' isolation guarantees that every part functions on its own, enhancing the overall dependability of the system. Over time, a codebase becomes easier to comprehend and maintain with a clear design and relevant test names. This method promotes a strong development process by lowering regressions and enhancing code quality. Furthermore, modular designs and well-defined test cases facilitate team member onboarding and debugging, encouraging cooperation. These procedures ultimately result in more scalable and resilient applications.