C# / .NET/05core11 min

Testing with xUnit

ProfessNet tests .NET code with xUnit — the default framework of the .NET ecosystem. For mocking we use Moq, and we write readable assertions with FluentAssertions. This lesson shows our patterns.

Anatomy of an xUnit test

[Fact] is a single test case. We stick to the AAA layout: 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);
    }
}

ProfessNet standard: the test name follows the Method_Condition_ExpectedResult scheme. One test = one behavior.

Theories — many cases

[Theory] with [InlineData] is a single test run for many data sets.

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

FluentAssertions — readable assertions

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

ProfessNet standard: we write assertions with FluentAssertions — the error message is readable and the intent is clear. We avoid long chains of raw Assert.Equal for complex objects.

Mocking with Moq

We replace external dependencies with a mock to test a unit in isolation.

[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);
}

Shared setup

We initialize shared state in the test class constructor (xUnit creates a new instance for each test — isolation is guaranteed).

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

    public ProbeServiceTests()
    {
        _sut = new ProbeService(_repo.Object, NullLogger<ProbeService>.Instance);
    }
}
Attribute / toolFor what
[Fact]a single case
[Theory] + [InlineData]many data sets, one test
Mock<T> (Moq)replacing a dependency
.Should() (FluentAssertions)readable assertions
IClassFixture<T>expensive setup shared across the class

Tip: _sut (system under test) is the name for the unit being tested — it makes reading easier. Only Verify a mock when the interaction is part of the contract, not an implementation detail.


xUnit + Moq + FluentAssertions is our standard toolkit. Fast unit tests on Application/Domain logic give confidence with every refactor and are an entry condition for a PR into the main branch.