iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

xUnit

xUnit.net is the de-facto C# test framework. Pair with FluentAssertions for readable expectations and Moq / NSubstitute for mocks. The pattern that scales: per-class fixtures, parameterised tests via [Theory], async by default.

xUnit + FluentAssertions + Moq patterns

EXAMPLE
using System;
using System.Threading.Tasks;
using FluentAssertions;
using Moq;
using Xunit;

// 1) System under test
public interface IOrderRepository
{
    Task<Order?> FindAsync(string id);
    Task SaveAsync(Order order);
}

public class Order
{
    public string Id { get; set; } = string.Empty;
    public string Status { get; set; } = "new";
}

public class CancelOrder
{
    private readonly IOrderRepository _repo;
    public CancelOrder(IOrderRepository repo) => _repo = repo;

    public async Task ExecuteAsync(string id)
    {
        var o = await _repo.FindAsync(id) ?? throw new ArgumentException("missing");
        if (o.Status == "cancelled") return;
        o.Status = "cancelled";
        await _repo.SaveAsync(o);
    }
}

// 2) Tests — one class per behaviour
public class CancelOrderTests
{
    private readonly Mock<IOrderRepository> _repo = new();

    [Fact]
    public async Task throws_when_order_is_missing()
    {
        _repo.Setup(r => r.FindAsync("o1")).ReturnsAsync((Order?)null);

        var act = async () => await new CancelOrder(_repo.Object).ExecuteAsync("o1");

        await act.Should().ThrowAsync<ArgumentException>().WithMessage("*missing*");
    }

    [Fact]
    public async Task is_idempotent_when_already_cancelled()
    {
        _repo.Setup(r => r.FindAsync("o1"))
             .ReturnsAsync(new Order { Id = "o1", Status = "cancelled" });

        await new CancelOrder(_repo.Object).ExecuteAsync("o1");

        _repo.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Never);
    }

    [Theory]
    [InlineData("new")]
    [InlineData("paid")]
    public async Task cancels_when_status_allows(string startingStatus)
    {
        var order = new Order { Id = "o1", Status = startingStatus };
        _repo.Setup(r => r.FindAsync("o1")).ReturnsAsync(order);

        await new CancelOrder(_repo.Object).ExecuteAsync("o1");

        order.Status.Should().Be("cancelled");
        _repo.Verify(r => r.SaveAsync(order), Times.Once);
    }
}

// 3) Per-test setup via IClassFixture (shared once per class)
public class DbFixture : IDisposable
{
    public string ConnectionString { get; } = "...";
    public DbFixture() { /* run migrations, seed */ }
    public void Dispose() { /* drop test DB */ }
}

public class OrdersIntegrationTests : IClassFixture<DbFixture>
{
    private readonly DbFixture _db;
    public OrdersIntegrationTests(DbFixture db) => _db = db;

    [Fact]
    public async Task example()
    {
        // exercise real DB via _db.ConnectionString
    }
}

// 4) ASP.NET Core integration tests with WebApplicationFactory
// public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
// {
//     private readonly HttpClient _client;
//     public OrdersApiTests(WebApplicationFactory<Program> factory)
//         => _client = factory.CreateClient();
//
//     [Fact]
//     public async Task health_returns_ok()
//     {
//         var res = await _client.GetAsync("/health");
//         res.IsSuccessStatusCode.Should().BeTrue();
//     }
// }

// 5) Async + cancellation testing
// [Fact]
// public async Task respects_cancellation()
// {
//     using var cts = new CancellationTokenSource(50);
//     var act = async () => await mySut.SlowAsync(cts.Token);
//     await act.Should().ThrowAsync<OperationCanceledException>();
// }

// ===== Patterns to internalise =====
// - One assertion per [Fact] when possible
// - Inject dependencies via constructor; the test wires them with mocks
// - [Theory] + [InlineData] for input variations
// - Per-class fixtures for expensive setup (DB, container)
// - FluentAssertions for readable failure messages
// - Async by default; never use .Result or .Wait in tests

// ===== Pitfalls =====
// - Hidden globals (DateTime.UtcNow) -> inject IClock for tests
// - Tests that depend on order -> isolate with per-test setup
// - Mock setups that return null without ReturnsAsync -> test surprises
// - Async tests that forget await -> false greens

Why it matters

`[Theory]` + `[InlineData]` covers parameterised tests cleanly without a separate fixture class per case. Combined with FluentAssertions for the read-aloud expectations and Moq for mocks, the test file reads as a description of behaviour rather than a stack of arrange/assert plumbing.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
public class CalcTests {
    [Fact]
    public void AddsNumbers() {
        Assert.Equal(5, new Calc().Add(2, 3));
    }
}
Try it Yourself »

Discussion

Loading…