C# / .NET/05core11 min

Testy z xUnit

ProfessNet testuje kod .NET w xUnit — domyślnym frameworku ekosystemu .NET. Do mockowania używamy Moq, a asercje piszemy czytelnie przez FluentAssertions. Ta lekcja pokazuje nasze wzorce.

Anatomia testu xUnit

[Fact] to pojedynczy przypadek testowy. Trzymamy się układu AAA: Arrange, Act, Assert.

public class ProbeTests
{
    [Fact]
    public void IsActive_WhenLastSeenRecent_ReturnsTrue()
    {
        // Arrange
        var probe = new Probe { LastSeen = DateTime.UtcNow.AddMinutes(-1) };

        // Act
        var result = probe.IsActive();

        // Assert
        Assert.True(result);
    }
}

Standard ProfessNet: nazwa testu w schemacie Metoda_Warunek_OczekiwanyWynik. Jeden test = jedno zachowanie.

Teorie — wiele przypadków

[Theory] z [InlineData] to jeden test uruchamiany dla wielu danych.

[Theory]
[InlineData("CORP", "corp")]
[InlineData(" Dmz ", "dmz")]
[InlineData("", "")]
public void Normalize_TrimsAndLowercases(string input, string expected)
{
    Assert.Equal(expected, ForestName.Normalize(input));
}

FluentAssertions — czytelne asercje

result.Should().BeTrue();
probes.Should().HaveCount(2).And.Contain(p => p.Host == "dc01");
act.Should().ThrowAsync<ProbeTimeoutException>();

Standard ProfessNet: asercje piszemy w FluentAssertions — komunikat błędu jest czytelny, a intencja jasna. Unikamy ciągów surowych Assert.Equal przy złożonych obiektach.

Mockowanie z Moq

Zewnętrzne zależności podmieniamy mockiem, żeby testować jednostkę w izolacji.

[Fact]
public async Task RunScanAsync_EnqueuesJob()
{
    // Arrange
    var repo = new Mock<IProbeRepository>();
    repo.Setup(r => r.GetHostsAsync("corp", It.IsAny<CancellationToken>()))
        .ReturnsAsync(new[] { "dc01" });

    var service = new ProbeService(repo.Object, NullLogger<ProbeService>.Instance);

    // Act
    var result = await service.RunScanAsync("corp");

    // Assert
    result.HostCount.Should().Be(1);
    repo.Verify(
        r => r.GetHostsAsync("corp", It.IsAny<CancellationToken>()),
        Times.Once);
}

Współdzielony setup

Wspólny stan inicjalizujemy w konstruktorze klasy testowej (xUnit tworzy nową instancję na każdy test — izolacja jest gwarantowana).

public class ProbeServiceTests
{
    private readonly Mock<IProbeRepository> _repo = new();
    private readonly ProbeService _sut;

    public ProbeServiceTests()
    {
        _sut = new ProbeService(_repo.Object, NullLogger<ProbeService>.Instance);
    }
}
Atrybut / narzędzieDo czego
[Fact]pojedynczy przypadek
[Theory] + [InlineData]wiele danych, jeden test
Mock<T> (Moq)podmiana zależności
.Should() (FluentAssertions)czytelne asercje
IClassFixture<T>drogi setup współdzielony w klasie

Wskazówka: _sut (system under test) to nazwa testowanej jednostki — ułatwia czytanie. Mock weryfikuj Verify tylko wtedy, gdy interakcja jest częścią kontraktu, a nie szczegółem implementacji.


xUnit + Moq + FluentAssertions to nasz standardowy zestaw. Szybkie testy jednostkowe na logice Application/Domain dają pewność przy każdym refaktorze i są warunkiem wejścia PR-a do głównej gałęzi.