.NET
07 / 08

Testing in .NET

.NET: Testing

xUnit Basics

// dotnet add package xunit xunit.runner.visualstudio
// dotnet add package Moq
// dotnet add package FluentAssertions
// dotnet add package Bogus  (fake data)

public class CalculatorTests
{
    [Fact]
    public void Add_TwoPositiveNumbers_ReturnsSum()
    {
        // Arrange
        var calc = new Calculator();
        // Act
        var result = calc.Add(3, 4);
        // Assert
        Assert.Equal(7, result);
    }

    [Theory]
    [InlineData(2, 3, 5)]
    [InlineData(-1, 1, 0)]
    [InlineData(0, 0, 0)]
    public void Add_VariousInputs_ReturnsCorrectSum(int a, int b, int expected)
    {
        var calc = new Calculator();
        Assert.Equal(expected, calc.Add(a, b));
    }

    [Theory]
    [ClassData(typeof(AddTestData))]
    public void Add_ClassData_Works(int a, int b, int expected) { }
}

public class AddTestData : TheoryData<int, int, int>
{
    public AddTestData()
    {
        Add(1, 2, 3);
        Add(10, 20, 30);
    }
}

Moq — Mocking

using Moq;

public class UserServiceTests
{
    private readonly Mock<IUserRepository> _repoMock = new();
    private readonly Mock<IEmailService> _emailMock = new();
    private readonly UserService _sut;

    public UserServiceTests()
    {
        _sut = new UserService(_repoMock.Object, _emailMock.Object,
            Mock.Of<ILogger<UserService>>());
    }

    [Fact]
    public async Task CreateAsync_ValidUser_SendsWelcomeEmail()
    {
        // Arrange
        var dto = new CreateUserDto("Alice", "alice@example.com", "password");
        _repoMock.Setup(r => r.ExistsAsync(dto.Email)).ReturnsAsync(false);
        _repoMock.Setup(r => r.CreateAsync(It.IsAny<User>()))
                 .ReturnsAsync((User u) => u);

        // Act
        var result = await _sut.CreateAsync(dto);

        // Assert
        _emailMock.Verify(e => e.SendWelcomeAsync(dto.Email), Times.Once);
        Assert.Equal("Alice", result.Name);
    }

    [Fact]
    public async Task CreateAsync_DuplicateEmail_ThrowsException()
    {
        _repoMock.Setup(r => r.ExistsAsync("alice@example.com")).ReturnsAsync(true);

        await Assert.ThrowsAsync<DuplicateEmailException>(
            () => _sut.CreateAsync(new CreateUserDto("Alice", "alice@example.com", "pass")));
    }
}

FluentAssertions

using FluentAssertions;

// Readable assertions — error messages show expected vs actual clearly
result.Should().Be(42);
result.Should().NotBeNull();
result.Should().BeGreaterThan(0).And.BeLessThan(100);

users.Should().HaveCount(3);
users.Should().ContainSingle(u => u.Name == "Alice");
users.Should().BeInAscendingOrder(u => u.Name);
users.Should().AllSatisfy(u => u.IsActive.Should().BeTrue());

user.Name.Should().StartWith("Al").And.HaveLength(5);
user.Email.Should().MatchRegex(@"^[^@]+@[^@]+\.[^@]+$");

// Exceptions
action.Should().Throw<ArgumentException>()
    .WithMessage("*cannot be empty*");

// Collections
list.Should().Contain(5).And.HaveCountGreaterThan(2);
list.Should().BeEquivalentTo(new[] { 1, 2, 3 });  // order-insensitive

Integration Tests with WebApplicationFactory

// dotnet add package Microsoft.AspNetCore.Mvc.Testing
// dotnet add package Testcontainers.PostgreSql  (real DB in Docker)

public class ApiIntegrationTests : IClassFixture<CustomWebApplicationFactory>
{
    private readonly HttpClient _client;

    public ApiIntegrationTests(CustomWebApplicationFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateUser_ValidRequest_Returns201()
    {
        var dto = new { Name = "Alice", Email = "alice@test.com", Password = "Password1!" };
        var response = await _client.PostAsJsonAsync("/api/users", dto);

        response.StatusCode.Should().Be(HttpStatusCode.Created);
        var user = await response.Content.ReadFromJsonAsync<UserDto>();
        user!.Name.Should().Be("Alice");
    }
}

public class CustomWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services => {
            // Replace real DB with test DB
            services.RemoveAll<DbContextOptions<AppDbContext>>();
            services.AddDbContext<AppDbContext>(opts =>
                opts.UseInMemoryDatabase("TestDb_" + Guid.NewGuid()));

            // Or use Testcontainers for real PostgreSQL
            // var pg = new PostgreSqlBuilder().Build();
            // pg.StartAsync().GetAwaiter().GetResult();
            // services.AddDbContext<AppDbContext>(opts => opts.UseNpgsql(pg.GetConnectionString()));
        });
    }
}

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free