C#
07 / 07

Testing & Benchmarking

C#: Testing & Benchmarking

xUnit Patterns

// Shared context — expensive setup once per class
public class DatabaseFixture : IDisposable
{
    public AppDbContext Db { get; }

    public DatabaseFixture()
    {
        var options = new DbContextOptionsBuilder<AppDbContext>()
            .UseInMemoryDatabase("SharedTestDb")
            .Options;
        Db = new AppDbContext(options);
        Db.Database.EnsureCreated();
        SeedTestData(Db);
    }

    public void Dispose() => Db.Dispose();
}

// IClassFixture — one instance per test class
public class UserRepositoryTests : IClassFixture<DatabaseFixture>
{
    private readonly AppDbContext _db;
    public UserRepositoryTests(DatabaseFixture fixture) => _db = fixture.Db;

    [Fact]
    public async Task GetByIdAsync_ExistingUser_ReturnsUser()
    {
        var repo = new UserRepository(_db);
        var user = await repo.GetByIdAsync(1);
        Assert.NotNull(user);
    }
}

// ICollectionFixture — shared across multiple test classes
[CollectionDefinition("Database")]
public class DatabaseCollection : ICollectionFixture<DatabaseFixture> { }

[Collection("Database")]
public class PostRepositoryTests { }

Test Builder Pattern

// Builder for complex test objects — avoids fragile constructors in tests
public class UserBuilder
{
    private string _name = "Test User";
    private string _email = "test@example.com";
    private string _role = "user";
    private bool _isActive = true;

    public UserBuilder WithName(string name) { _name = name; return this; }
    public UserBuilder WithEmail(string email) { _email = email; return this; }
    public UserBuilder AsAdmin() { _role = "admin"; return this; }
    public UserBuilder Inactive() { _isActive = false; return this; }

    public User Build() => new() {
        Id = Random.Shared.Next(1, 10000),
        Name = _name,
        Email = _email,
        Role = _role,
        IsActive = _isActive,
    };
}

// Bogus — fake data generation (more powerful)
// dotnet add package Bogus
var faker = new Faker<User>()
    .RuleFor(u => u.Id, f => f.IndexFaker + 1)
    .RuleFor(u => u.Name, f => f.Name.FullName())
    .RuleFor(u => u.Email, (f, u) => f.Internet.Email(u.Name))
    .RuleFor(u => u.CreatedAt, f => f.Date.Past());

var user = faker.Generate();
var users = faker.Generate(100);

BenchmarkDotNet

// dotnet add package BenchmarkDotNet
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]      // reports allocations
[SimpleJob(RuntimeMoniker.Net90)]
public class StringBenchmarks
{
    private const int N = 1000;
    private readonly string[] _words = Enumerable.Range(0, N)
        .Select(i => $"word{i}").ToArray();

    [Benchmark]
    public string Concatenation()
    {
        var result = string.Empty;
        foreach (var word in _words) result += word + " ";
        return result;
    }

    [Benchmark]
    public string StringBuilder()
    {
        var sb = new System.Text.StringBuilder();
        foreach (var word in _words) sb.Append(word).Append(' ');
        return sb.ToString();
    }

    [Benchmark]
    public string StringJoin() => string.Join(' ', _words);
}

// Run benchmarks (must be Release mode)
// dotnet run -c Release
BenchmarkRunner.Run<StringBenchmarks>();

// Output includes: Mean, StdDev, Gen0/1/2 GC, Allocated bytes

Code Coverage & Best Practices

  • Coverage: dotnet test --collect:"XPlat Code Coverage" — generates coverage.cobertura.xml. Use ReportGenerator to view HTML.

  • AAA pattern: every test has Arrange (setup), Act (execute), Assert (verify) — never mix.

  • One assert per test: one logical concept per test — easier to diagnose failures.

  • Test names: MethodName_Scenario_ExpectedResult — e.g., GetById_UserNotFound_ReturnsNull.

  • Avoid testing implementation details — test behavior (outputs/side effects), not internals.

  • Mutation testing: Stryker.NET — mutates your code and checks if tests catch it; reveals weak test coverage.

  • Test in all three layers: unit (fast, isolated), integration (real DB/HTTP), E2E (Playwright for UI).

  • Mock only what you own: don't mock third-party libraries directly. Wrap them behind an interface.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free