All articles

C# Interview Guide: Unit Testing with xUnit, NSubstitute, and FluentAssertions

3 min read
#0
0 reads
0 reading now
0 shares

Q24 — Unit Testing with xUnit, NSubstitute, FluentAssertions — Full Examples

Level: Mid-Level | Topic: Testing

The Testing Pyramid

A healthy test suite follows the pyramid: ~70% unit tests (milliseconds, no I/O, xUnit + NSubstitute + FluentAssertions), ~20% integration tests (seconds, real DB via Testcontainers), ~10% E2E/contract tests (minutes, full stack). Most bugs are caught cheapest at the unit level.

The AAA Pattern

Every test follows Arrange → Act → Assert. NSubstitute creates fakes of interfaces. FluentAssertions provides readable failure messages. [Theory] + [InlineData] parameterises tests for multiple inputs without duplication.

Code Example

// Packages: xunit, NSubstitute, FluentAssertions
public class OrderServiceTests
{
    // Shared fakes — recreated per test (xUnit creates a new instance per [Fact])
    private readonly IOrderRepository _repo   = Substitute.For<IOrderRepository>();
    private readonly IEventPublisher   _events = Substitute.For<IEventPublisher>();
    private readonly OrderService _sut;

    public OrderServiceTests()
        => _sut = new OrderService(_repo, _events, NullLogger<OrderService>.Instance);

    // ── Happy path ──────────────────────────────────────────────
    [Fact]
    public async Task CreateAsync_WithValidRequest_ShouldSaveAndPublishEvent()
    {
        // Arrange
        var request = new CreateOrderRequest("cust-123", 99.99m);
        _repo.SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
             .Returns(Task.CompletedTask);

        // Act
        var result = await _sut.CreateAsync(request, CancellationToken.None);

        // Assert — FluentAssertions: readable failure messages
        result.Should().NotBeNull();
        result.CustomerId.Should().Be("cust-123");
        result.Total.Should().Be(99.99m);
        result.Status.Should().Be("Pending");

        // Verify interactions — was SaveAsync called exactly once?
        await _repo.Received(1).SaveAsync(
            Arg.Is<Order>(o => o.CustomerId == "cust-123"),
            Arg.Any<CancellationToken>());

        await _events.Received(1).PublishAsync(
            Arg.Is<OrderCreatedEvent>(e => e.CustomerId == "cust-123"),
            Arg.Any<CancellationToken>());
    }

    // ── Parameterised validation tests ─────────────────────────
    [Theory]
    [InlineData("", 99.99)]
    [InlineData(null, 99.99)]
    [InlineData("cust-1", 0)]
    [InlineData("cust-1", -1)]
    public async Task CreateAsync_WithInvalidInput_ShouldThrowValidationException(
        string? customerId, decimal total)
    {
        var request = new CreateOrderRequest(customerId!, total);

        await _sut.Invoking(s => s.CreateAsync(request, CancellationToken.None))
            .Should().ThrowAsync<ValidationException>();

        // Nothing should be saved on invalid input
        await _repo.DidNotReceive().SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>());
    }

    // ── Infrastructure failure doesn't publish event ───────────
    [Fact]
    public async Task CreateAsync_WhenRepositoryThrows_ShouldNotPublishEvent()
    {
        var request = new CreateOrderRequest("cust-1", 50m);
        _repo.SaveAsync(Arg.Any<Order>(), Arg.Any<CancellationToken>())
             .ThrowsAsync(new Exception("DB connection failed"));

        var act = async () => await _sut.CreateAsync(request, CancellationToken.None);

        await act.Should().ThrowAsync<Exception>();
        await _events.DidNotReceive()
            .PublishAsync(Arg.Any<OrderCreatedEvent>(), Arg.Any<CancellationToken>());
    }
}

Senior Insight

Name tests with the pattern MethodName_Condition_ExpectedOutcome — it reads as living documentation. Use NullLogger<T>.Instance rather than mocking ILogger — you rarely need to assert on log calls, and the mock noise obscures test intent. Prefer Arg.Is<T>(predicate) over Arg.Any<T>() in verification calls — it catches wrong data being passed to dependencies. Keep each test focused on one behaviour.

ShareLinkedInX

Comments

Share your thoughts without signing in.