xUnit Testing & CI/CD in .NET

xUnit Testing & CI/CD in .NET

Explore xunit testing & ci/cd in .net code snippets and tutorials

xUnit Testing & CI/CD in .NET

xUnit test output, traits and skipping tests: ITestOutputHelper, [Trait] and dotnet test --filter

Write test output through ITestOutputHelper (injected into the constructor) instead of Console.WriteLine, tag tests with [Trait] so you can include or exclude them with dotnet test --filter, and skip a …

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ======================================================================
// Q: Test output, categories and skipping in xUnit?
//
// Write test output through ITestOutputHelper (injected into the constructor)
// instead of Console.WriteLine, tag tests with [Trait] so you can include or
// exclude them with dotnet test --filter, and skip a test temporarily with Skip
// and a reason that points to a ticket.
// ======================================================================

public class ImportTests(ITestOutputHelper output)     // instead of Console.WriteLine
{
    [Fact]
    [Trait("Category", "Slow")]
    public void Imports_large_file()
    {
        output.WriteLine($"Imported {count} rows");
    }

    [Fact(Skip = "ERP sandbox down until 15/11, ticket SHOP-482")]
    public void Sends_to_real_erp_sandbox() { }
}

dotnet test --filter "Category!=Slow"                 # without the slow tests
dotnet test --filter "FullyQualifiedName~Orders"      # only tests with "Orders" in the name
xUnit Testing & CI/CD in .NET

.NET code coverage with coverlet and ReportGenerator, and mutation testing with Stryker.NET

Coverage tells you which code was executed, not that it was verified.

bash
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# ======================================================================
# Q: Code coverage: what target do you set? How do you know your tests are good?
#
# - Coverage tells you which code was executed, not that it was verified. A test
#   with no assert still produces coverage.
# - A sensible target: high coverage in the domain / application layers, not
#   100% everywhere. In CI: a threshold for new code, so coverage doesn't drop.
# - Mutation testing (Stryker): it changes the code (e.g. > to >=) and checks
#   whether some test fails. If none does, the tests don't verify that rule.
# ======================================================================

dotnet test --collect:"XPlat Code Coverage"          # coverlet → coverage.cobertura.xml
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage-report
dotnet stryker                                         # mutation testing (Stryker.NET)
xUnit Testing & CI/CD in .NET

xUnit Assert vs FluentAssertions vs Shouldly in C#, plus snapshot testing with Verify

Fluent libraries give more readable failure messages.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ======================================================================
// Q: xUnit Assert, FluentAssertions or Shouldly?
//
// - Fluent libraries give more readable failure messages. Caution:
//   FluentAssertions v8 moved to a paid license for commercial use. Many teams
//   stayed on v7 or switched to Shouldly / AwesomeAssertions.
// - Snapshot testing (Verify) for large outputs (JSON responses, generated SQL,
//   emails): the first run stores the result, later runs compare against it.
// ======================================================================

// xUnit
Assert.Equal(2, order.Lines.Count);
Assert.Collection(order.Lines,
    l => Assert.Equal("PIP-100", l.Sku),
    l => Assert.Equal("GLV-NIT-M", l.Sku));
Assert.Equivalent(new { Sku = "PIP-100", Quantity = 2 }, dto);    // structural comparison

// Shouldly
order.Status.ShouldBe(OrderStatus.Placed);
order.Lines.ShouldContain(l => l.Sku == "PIP-100");
xUnit Testing & CI/CD in .NET

Creating C# test data without repetition: Test Data Builder, Object Mother, AutoFixture, Bogus

Test Data Builder (Q27): valid defaults, and you change only what matters to the test.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// ======================================================================
// Q: How do you create test data without repetition?
//
// - Test Data Builder (Q27): valid defaults, and you change only what matters to
//   the test. The test shows what counts.
// - Object Mother: ready-made, named objects (TestOrders.PlacedWithTwoLines()).
// - AutoFixture (random values for whatever doesn't matter), Bogus (realistic
//   names and addresses for demo / seed data).
// ======================================================================

var order = new OrderBuilder()
    .ForCustomer(1)
    .WithLine("PIP-100", quantity: 2)
    .Placed()
    .Build();                // everything else: valid defaults
xUnit Testing & CI/CD in .NET

ILogger and IOptions in .NET unit tests: NullLogger, FakeLogger and Options.Create

Use NullLogger<T>.Instance when the logs don't matter, FakeLogger<T> (Microsoft.Extensions.Diagnostics.Testing) when you need to assert that something was logged, and Options.Create to build options without any configuration.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// ======================================================================
// Q: How do you handle ILogger and IOptions in unit tests?
//
// Use NullLogger<T>.Instance when the logs don't matter, FakeLogger<T>
// (Microsoft.Extensions.Diagnostics.Testing) when you need to assert that
// something was logged, and Options.Create to build options without any
// configuration.
// ======================================================================

// You don't care about the logs
var sut = new PriceSync(repo, NullLogger<PriceSync>.Instance);

// You want to verify that a warning was logged (Microsoft.Extensions.Diagnostics.Testing)
var logger = new FakeLogger<SyncOrderToErpHandler>();
// ... act ...
Assert.Equal(LogLevel.Warning, logger.Collector.LatestRecord.Level);
Assert.Contains("rejected by ERP", logger.Collector.LatestRecord.Message);

// Options without configuration
var options = Options.Create(new ErpOptions { BaseUrl = "http://erp/", AttemptTimeoutSeconds = 5 });