--- name: dotnet-testing description: Mandatory testing conventions for .NET (xUnit, AwesomeAssertions). Use for unit, integration, or acceptance tests with xUnit, Testcontainers, or Playwright. --- # Testing Conventions (.NET) This skill defines the **MANDATORY** testing conventions for .NET projects. These are **rules**, not guidelines. --- ## Documentation Rules ### NO Comments Except AAA Markers * **NEVER write XML summaries** on test methods or test classes * **NEVER write explanatory comments**: code must be self-explanatory * **Only `// Arrange`, `// Act`, `// Assert`** comments are allowed in tests * The test name `Should_X_When_Y` already documents the intent --- ## Libraries | Category | Library | Purpose | | -------- | ------- | ------- | | Framework | xUnit | Test framework | | Assertions | AwesomeAssertions | Fluent assertion syntax | | Snapshots | Verify.Xunit | Snapshot / approval testing | | Mocking | NSubstitute | Spies, stubs, and mocks | | Mocking (HTTP) | WireMock.Net | HTTP service mocking | | Test data (fake) | Bogus | Realistic fake data (Mother pattern) | | Test data (dummy) | AutoFixture | Automatic dummy object generation | | Containers | Testcontainers | Docker containers in tests | --- ## Core Principles ### 1. AAA Pattern (Arrange – Act – Assert) **ALL tests MUST follow the AAA pattern**, separated by inline comments. #### Rules * Each phase **MUST** be separated with comments * **Never mix phases** * **Each comment (`// Arrange`, `// Act`, `// Assert`) appears AT MOST ONCE per test**: if you need two actions or two asserts, write two tests * **Act = one single invocation on the SUT.** Multiple statements in Act only if they are genuinely part of the same logical action (rare and exceptional). Two independent operations = two tests. * **All assertions belong in Assert only.** No `.Should()` in Arrange or Act. If you need a precondition check, extract it to a separate test or use a guard clause that throws: not an assertion. * **AAA markers are mandatory in ALL tests**: including structural guards, static completeness checks, and data validation tests. No exceptions. * **No `if`, `switch`, or conditional logic** inside Arrange, Act, or Assert * **No `try/catch/finally`** inside tests: use `IAsyncLifetime` or `IDisposable` for teardown * **No `// Act & Assert` combined blocks**: Act and Assert are ALWAYS separate * For exceptions: `act.Should().ThrowAsync()` (AwesomeAssertions) * Omit comment if section is empty * If a test needs branching, split it into separate tests #### xUnit Example ```csharp [Fact(Timeout = CancellationTokenForTest.ShortTimeout)] public async Task Should_CreateGroup_When_RequestIsValid() { // Arrange var request = new CreateGroupRequest { Name = "Test Group" }; var command = new CreateGroupCommand(request); // Act var actual = await _sut.Handle(command, CancellationToken.None); // Assert actual.Should().NotBeNull(); actual.GroupId.Should().NotBeEmpty(); _repository.Received(1).AddGroup(Arg.Any()); } ``` --- ### 2. Test Naming Convention ```text Should_{ExpectedBehavior}_When_{Condition} ``` #### Rules * **PascalCase** for both parts * **NO** natural language * **NO** vague names (`Should_Work`, `TestHandler`) * **NO** missing `When` clause * Method names only: xUnit uses `[Fact]` or `[Theory]`, not description strings --- ### 3. Variable Naming (MANDATORY) | Purpose | Name | | ------- | ---- | | Subject under test | `_sut` (field) or `sut` (local) | | Expected value | `expected` | | Actual result | `actual` | --- ## Test Class Structure ```csharp public class CreateGroupCommandHandlerTests { private readonly Fixture _fixture; private readonly IEnvilderRepository _repository; private readonly CreateGroupCommandHandler _sut; public CreateGroupCommandHandlerTests() { _fixture = new(); _repository = Substitute.For(); _sut = new(_repository); } } ``` --- ## Exception Testing **Act and Assert MUST be separate.** ### ✅ CORRECT ```csharp [Fact(Timeout = CancellationTokenForTest.ShortTimeout)] public async Task Should_ThrowNotFound_When_GroupDoesNotExist() { // Arrange _repository.GetByIdAsync(Arg.Any(), Arg.Any()) .Returns((Group?)null); // Act var act = () => _sut.Handle(query, CancellationToken.None); // Assert await act.Should().ThrowAsync(); } ``` ### ❌ FORBIDDEN ```csharp // Act & Assert ← NEVER await Assert.ThrowsAsync( () => _sut.Handle(query, CancellationToken.None)); ``` --- ## Teardown & Cleanup **Never use `try/finally` in tests.** Use framework mechanisms: | Scenario | Mechanism | | -------- | --------- | | Async setup/teardown | `IAsyncLifetime` (`InitializeAsync` / `DisposeAsync`) | | Sync cleanup | `IDisposable` (`Dispose`) | | Shared fixture | `IAsyncLifetime` on collection fixture | ```csharp public class MyTests : IAsyncLifetime { private (string Name, string? Value)[] _originalEnvValues = []; public Task InitializeAsync() { _originalEnvValues = EnvVarNames .Select(v => (Name: v, Value: Environment.GetEnvironmentVariable(v))) .ToArray(); return Task.CompletedTask; } public Task DisposeAsync() { foreach (var (name, value) in _originalEnvValues) { Environment.SetEnvironmentVariable(name, value); } return Task.CompletedTask; } } ``` --- ## Verification Commands | Context | Command | | ------- | ------- | | Unit tests | `dotnet test tests/sdks/dotnet/ --filter "Category!=Acceptance"` | | Acceptance (Docker) | `dotnet test tests/sdks/dotnet/` | | Format check | `dotnet format src/sdks/dotnet/Envilder.sln --verify-no-changes` | --- ## Completion Criteria * Test names follow `Should__When_` * AAA markers present (except structural guards), each at most once * `_sut`/`actual`/`expected` used consistently * All mock interactions verified with `.Received()` * No `try/catch/finally` or control flow in test bodies * Tests run green: `dotnet test` * No format regressions: `dotnet format --verify-no-changes`