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 [Fact] vs [Theory]: InlineData, MemberData and TheoryData in C#

[Fact]: a test without parameters, one scenario.

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
25
26
27
28
29
30
31
32
33
34
35
// ======================================================================
// Q: [Fact] vs [Theory]? How do you pass test data?
//
// [Fact]: a test without parameters, one scenario. [Theory]: the same test with
// multiple data sets. Each data set shows up as a separate test.
// There is also [ClassData(typeof(...))] for data in a separate class.
// InlineData only accepts compile-time constants (no decimal, no objects): hence
// TheoryData.
// ======================================================================

[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(10_001)]
public void AddLine_rejects_invalid_quantity(int quantity)
{
    var order = new OrderBuilder().Build();
    Assert.Throws<DomainException>(() => order.AddLine("PIP-100", quantity, Money.Eur(10m)));
}

// Complex data: TheoryData<T> (strongly typed) with MemberData
public static TheoryData<string, decimal, string, decimal> ShippingCases => new()
{
    { "dhl", 2.5m, "DE", 8.90m },
    { "dhl", 2.5m, "GR", 16.40m },     // +7.50 outside Germany
    { "ups", 1.0m, "DE", 8.40m },
};

[Theory]
[MemberData(nameof(ShippingCases))]
public void Calculates_shipping_cost(string carrier, decimal kg, string country, decimal expected)
{
    var calculator = factory.Create(carrier);
    Assert.Equal(expected, calculator.Calculate(new ShipmentRequest(kg, country)).Amount);
}
xUnit Testing & CI/CD in .NET

xUnit unit testing best practices in C#: AAA pattern, naming and FIRST

AAA: Arrange (setup), Act (one action), Assert (check the result).

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
25
26
27
28
29
30
// ======================================================================
// Q: What makes a good unit test? How do you structure it?
//
// - AAA: Arrange (setup), Act (one action), Assert (check the result).
// - One behavior per test, with a name that describes it:
//   Method_Scenario_ExpectedResult or a sentence (Cannot_place_an_empty_order).
// - FIRST: Fast, Isolated (no shared state), Repeatable (same result
//   everywhere), Self-validating (pass/fail without human inspection), Timely.
// - Test behavior (result, state, observable side effects), not implementation
//   details. A refactoring that does not change behavior should not break tests.
// - Unit tests for domain logic and rules. Integration tests for the database,
//   HTTP, serialization, DI (Q13).
// ======================================================================

public class OrderTests
{
    [Fact]
    public void Place_with_lines_moves_order_to_placed_and_raises_event()
    {
        // Arrange
        var order = new OrderBuilder().WithLine("PIP-100", 2, unitPrice: 189m).Build();

        // Act
        order.Place();

        // Assert
        Assert.Equal(OrderStatus.Placed, order.Status);
        Assert.Single(order.DomainEvents.OfType<OrderPlaced>());
    }
}
xUnit Testing & CI/CD in .NET

Secrets in Azure for .NET apps: Key Vault with Managed Identity and DefaultAzureCredential

In Azure Key Vault, accessed through Managed Identity: no connection strings or passwords in the repo or pipeline.

csharp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// ======================================================================
// Q: Where do secrets live in Azure?
//
// In Azure Key Vault, accessed through Managed Identity: no connection strings
// or passwords in the repo or pipeline. For the database: Entra ID
// authentication where possible. Locally: dotnet user-secrets.
// Observability: Application Insights / OpenTelemetry, structured logging
// without PII, health checks (/health/ready checking the DB and the ERP), alerts
// on outbox and DLQ size.
// ======================================================================

builder.Configuration.AddAzureKeyVault(
    new Uri(builder.Configuration["KeyVault:Uri"]!), new DefaultAzureCredential());
xUnit Testing & CI/CD in .NET

CI/CD pipeline for an ASP.NET Core service: PR checks, Docker image per commit, Azure deploy

On every PR: build, unit and integration tests, analyzers with warnings-as-errors, dependency scan (dotnet list package --vulnerable), secret scan, mandatory human review.

docker
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ======================================================================
# Q: Describe a good CI/CD pipeline for this service.
#
# - On every PR: build, unit and integration tests, analyzers with warnings-as-
#   errors, dependency scan (dotnet list package --vulnerable), secret scan,
#   mandatory human review.
# - On merge to main: build the Docker image once, tagged with the commit SHA.
#   The same image is promoted from staging to prod.
# - Deployment: migration script, deploy to Azure App Service (slot swap) or
#   Container Apps, smoke test, automatic rollback.
# ======================================================================

# Multi-stage Dockerfile for the .NET 9 API (non-root runtime)
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app --no-restore

FROM mcr.microsoft.com/dotnet/aspnet:9.0
WORKDIR /app
COPY --from=build /app .
USER $APP_UID            # non-root
ENTRYPOINT ["dotnet", "Api.dll"]
xUnit Testing & CI/CD in .NET

ASP.NET Core integration tests with xUnit, WebApplicationFactory and Testcontainers PostgreSQL

Integration tests with WebApplicationFactory and Testcontainers, against a real PostgreSQL.

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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// ======================================================================
// Q: How do you test an API that uses PostgreSQL?
//
// Integration tests with WebApplicationFactory and Testcontainers, against a
// real PostgreSQL. Not the InMemory provider: it doesn't check constraints,
// transactions or SQL.
// An authorization test like the one above is exactly what AI won't write on its
// own. Mentioning it earns you points.
// ======================================================================

public class ApiFactory : WebApplicationFactory<Program>, IAsyncLifetime
{
    private readonly PostgreSqlContainer _pg = new PostgreSqlBuilder()
        .WithImage("postgres:16-alpine").Build();

    protected override void ConfigureWebHost(IWebHostBuilder b) =>
        b.ConfigureTestServices(s =>
        {
            s.RemoveAll<DbContextOptions<AppDbContext>>();
            s.AddDbContext<AppDbContext>(o => o.UseNpgsql(_pg.GetConnectionString()));
        });

    public async Task InitializeAsync()
    {
        await _pg.StartAsync();
        using var scope = Services.CreateScope();
        await scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.MigrateAsync();
    }
    public new Task DisposeAsync() => _pg.DisposeAsync().AsTask();
}

public class OrdersTests(ApiFactory f) : IClassFixture<ApiFactory>
{
    [Fact]
    public async Task Customer_cannot_read_other_customers_order()
    {
        var client = f.CreateClientAs(customerId: 1);   // helper with a test auth handler
        var res = await client.GetAsync("/api/orders/999"); // belongs to customer 2
        Assert.Equal(HttpStatusCode.NotFound, res.StatusCode);
    }
}