Isaac.

Unit Testing in ASP.NET Core

Learn how to write effective unit tests for ASP.NET Core applications using xUnit, Moq, and best practices.

By EMEPublished: February 20, 2025
aspnetunit testingxunitmoqtestingmockingtdd

A Simple Analogy

Imagine building a house where you check each brick before laying it. Unit testing is like that—you test small pieces (functions, methods) in isolation before putting everything together. If a brick is bad, you find it immediately rather than discovering cracks in the wall months later.


What Is Unit Testing?

Unit testing is the practice of writing automated tests for small, individual pieces of code (functions, methods, classes) in isolation. Each test verifies that a specific unit behaves correctly under given conditions.


Why Use Unit Testing?

  • Catch bugs early: Find problems during development, not production
  • Refactoring confidence: Change code without breaking things
  • Living documentation: Tests show how code is supposed to work
  • Faster debugging: Failed test pinpoints the exact issue
  • Code quality: Forces you to write testable, modular code
  • Regression prevention: Catch when old bugs come back

Test Structure: Arrange-Act-Assert (AAA)

Every good unit test follows this pattern:

1. Arrange: Set up test data and mocks
2. Act: Call the method being tested
3. Assert: Verify the result is correct

Setting Up xUnit and Moq

Install Packages

dotnet add package xunit
dotnet add package xunit.runner.visualstudio
dotnet add package Moq
dotnet add package Microsoft.NET.Test.Sdk

Create Test Project

dotnet new xunit -n MyApp.Tests

Basic Unit Test Example

Code to Test

public class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
}

Unit Tests

public class CalculatorTests
{
    [Fact]
    public void Add_TwoPositiveNumbers_ReturnsSum()
    {
        // Arrange
        var calculator = new Calculator();
        int a = 5;
        int b = 3;

        // Act
        int result = calculator.Add(a, b);

        // Assert
        Assert.Equal(8, result);
    }

    [Fact]
    public void Subtract_LargerMinusSmallerNumber_ReturnsPositiveDifference()
    {
        // Arrange
        var calculator = new Calculator();

        // Act
        int result = calculator.Subtract(10, 3);

        // Assert
        Assert.Equal(7, result);
    }
}

Testing with Dependencies (Mocking)

Code with Dependencies

public interface IEmailService
{
    Task SendAsync(string email, string message);
}

public class UserService
{
    private readonly IUserRepository _repo;
    private readonly IEmailService _emailService;

    public UserService(IUserRepository repo, IEmailService emailService)
    {
        _repo = repo;
        _emailService = emailService;
    }

    public async Task RegisterUserAsync(string name, string email)
    {
        var user = new User { Name = name, Email = email };
        await _repo.AddAsync(user);
        await _emailService.SendAsync(email, $"Welcome, {name}!");
    }
}

Unit Test with Mocks

public class UserServiceTests
{
    [Fact]
    public async Task RegisterUserAsync_ValidUser_SavesAndSendsEmail()
    {
        // Arrange
        var mockRepo = new Mock<IUserRepository>();
        var mockEmailService = new Mock<IEmailService>();

        var service = new UserService(mockRepo.Object, mockEmailService.Object);

        // Act
        await service.RegisterUserAsync("Alice", "alice@example.com");

        // Assert
        mockRepo.Verify(
            r => r.AddAsync(It.IsAny<User>()),
            Times.Once,
            "User should be added to repository");

        mockEmailService.Verify(
            e => e.SendAsync("alice@example.com", It.IsAny<string>()),
            Times.Once,
            "Welcome email should be sent");
    }
}

Parameterized Tests (Test Multiple Scenarios)

[Theory]
[InlineData(2, 3, 5)]
[InlineData(0, 0, 0)]
[InlineData(-1, 1, 0)]
[InlineData(100, 200, 300)]
public void Add_VariousNumbers_ReturnsCorrectSum(int a, int b, int expected)
{
    // Arrange
    var calc = new Calculator();

    // Act
    var result = calc.Add(a, b);

    // Assert
    Assert.Equal(expected, result);
}

Testing Exceptions

[Fact]
public void Divide_ByZero_ThrowsException()
{
    // Arrange
    var calc = new Calculator();

    // Act & Assert
    var exception = Assert.Throws<DivideByZeroException>(
        () => calc.Divide(10, 0));

    Assert.NotNull(exception);
}

Integration Testing (Optional but Useful)

[Fact]
public async Task CreateUser_ValidData_ReturnCreatedAtRoute()
{
    // Arrange
    using var client = new HttpClient { BaseAddress = new Uri("https://localhost") };
    var userData = new { Name = "Bob", Email = "bob@example.com" };

    // Act
    var response = await client.PostAsJsonAsync("/api/users", userData);

    // Assert
    response.StatusCode.Should().Be(HttpStatusCode.Created);
    var location = response.Headers.Location;
    location.Should().NotBeNull();
}

Practical Examples

Testing a Service

public class OrderServiceTests
{
    [Fact]
    public async Task CreateOrder_ValidItems_ReturnsOrderWithTotalPrice()
    {
        // Arrange
        var mockRepo = new Mock<IOrderRepository>();
        var service = new OrderService(mockRepo.Object);

        var items = new[] { 
            new OrderItem { Price = 10, Quantity = 2 },
            new OrderItem { Price = 5, Quantity = 3 }
        };

        // Act
        var order = await service.CreateOrderAsync(items);

        // Assert
        Assert.Equal(35, order.TotalPrice);
        mockRepo.Verify(r => r.SaveAsync(order), Times.Once);
    }
}

Testing a Controller

[HttpGet("{id}")]
public async Task<IActionResult> GetProduct(int id)
{
    var product = await _service.GetProductAsync(id);
    if (product == null) return NotFound();
    return Ok(product);
}

// Test
public class ProductControllerTests
{
    [Fact]
    public async Task GetProduct_WithValidId_ReturnsOkWithProduct()
    {
        // Arrange
        var mockService = new Mock<IProductService>();
        mockService
            .Setup(s => s.GetProductAsync(1))
            .ReturnsAsync(new Product { Id = 1, Name = "Widget" });

        var controller = new ProductController(mockService.Object);

        // Act
        var result = await controller.GetProduct(1);

        // Assert
        var okResult = Assert.IsType<OkObjectResult>(result);
        var product = Assert.IsType<Product>(okResult.Value);
        Assert.Equal("Widget", product.Name);
    }
}

Real-World Use Cases

  • E-commerce: Test order processing, payment validation, inventory updates
  • APIs: Test endpoint logic, authentication, error handling
  • Business logic: Test calculations, rules, workflows
  • Data access: Test repository methods with mocked databases
  • Services: Test external integrations (email, SMS, payments)

Best Practices

  • One assertion per test: Makes it clear what failed
  • Descriptive names: Test names should explain what they test
  • Keep tests independent: Tests shouldn't depend on each other
  • Mock external dependencies: Only test what you wrote
  • Avoid brittle tests: Don't test implementation details
  • Test behavior, not code: Focus on what should happen, not how
  • Keep tests fast: They should run in milliseconds
  • Maintain test code: Refactor tests like production code

Common Pitfalls

  • Testing implementation details instead of behavior
  • Over-mocking (mocking too many things)
  • Slow tests (use async mocks, avoid real databases)
  • Unclear test names (use Fact_Should_Scenario naming)
  • Not testing edge cases or error conditions
  • Too many assertions (makes debugging hard)

Related Concepts to Explore

  • Test-Driven Development (TDD)
  • Integration testing
  • End-to-end (E2E) testing
  • Test coverage and code coverage tools
  • Continuous Integration (CI) pipelines
  • Behavior-Driven Development (BDD)
  • Performance testing
  • Load testing
  • Mocking frameworks and stubs
  • Test fixtures and setup/teardown

Summary

Unit testing is essential for writing reliable, maintainable code. By testing small units in isolation with clear Arrange-Act-Assert patterns and proper mocking, you build confidence in your code, catch bugs early, and make refactoring safe. Start with simple tests, gradually add complexity, and you'll find bugs before your users do.