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…