.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-insensitiveIntegration 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