Writing Unit Tests for ASP.NET Core APIs in C#

June 1, 2025

Summary

Unit testing is one of the most important practices when building APIs. It ensures that your code works as intended, prevents regressions, and gives you confidence when refactoring or deploying to production. In this post, we’ll walk through writing unit tests for an ASP.NET Core API using xUnit and Moq.

Challenge

Without tests, it’s easy to accidentally break your API when making changes. Common problems include:

  • Controllers tied too tightly to database logic (hard to test)
  • Business logic hidden inside controllers
  • Lack of mocks, making tests slow or flaky

Solution

The best practice is to keep controllers thin, move logic into services, and test those services independently with unit tests. For API testing, we’ll use xUnit (test framework) and Moq (for mocking dependencies).

Code Example

Let’s say we have a simple ProductService:

public interface IProductService
{
    Task<Product?> GetProductByIdAsync(int id);
}

public class ProductService : IProductService
{
    private readonly IProductRepository _repo;

    public ProductService(IProductRepository repo)
    {
        _repo = repo;
    }

    public async Task<Product?> GetProductByIdAsync(int id)
    {
        if (id <= 0) return null; // basic guard clause
        return await _repo.GetByIdAsync(id);
    }
}

Here’s how to test it with xUnit + Moq:

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

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

    [Fact]
    public async Task GetProductByIdAsync_ShouldReturnProduct_WhenValidId()
    {
        // Arrange
        var product = new Product { Id = 1, Name = "Test Product" };
        _mockRepo.Setup(r => r.GetByIdAsync(1)).ReturnsAsync(product);

        // Act
        var result = await _service.GetProductByIdAsync(1);

        // Assert
        Assert.NotNull(result);
        Assert.Equal("Test Product", result!.Name);
    }

    [Fact]
    public async Task GetProductByIdAsync_ShouldReturnNull_WhenInvalidId()
    {
        // Act
        var result = await _service.GetProductByIdAsync(0);

        // Assert
        Assert.Null(result);
    }
}

Takeaways

  • Keep controllers thin → move logic into services to make testing easier.
  • Use xUnit for test cases and Moq to mock dependencies.
  • Test both happy paths (valid input) and edge cases (invalid input).
  • With proper unit tests, you can refactor APIs confidently without fear of breaking functionality.

💭 In a future post, we’ll look at integration testing ASP.NET Core APIs using the WebApplicationFactory for end-to-end scenarios.