
Qa Testing Nunit
- 87 installs
- 73 repo stars
- Updated July 13, 2026
- vasilyu1983/ai-agents-public
Helps with testing & qa tasks.
About
qa-testing-nunit is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.
- qa-testing-nunit
- Testing & QA
- AI-coding skill
Qa Testing Nunit by the numbers
- 87 all-time installs (skills.sh)
- +9 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,049 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/vasilyu1983/ai-agents-public --skill qa-testing-nunitAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 87 |
|---|---|
| repo stars | ★ 73 |
| Last updated | July 13, 2026 |
| Repository | vasilyu1983/ai-agents-public ↗ |
What it does
Helps with testing & qa tasks.
Files
C# Testing NUnit Fixtures
Quick Reference
- Classify test scope first: API, component, or integration.
- Lock runtime constraints before execution: Docker availability, framework target, and explicitly excluded suites.
- Use this skill for test-suite architecture and fixture behavior, not for general service implementation or CI graph refactors.
- Default to two files per handler/use case:
<Feature>Fixture.csand<Feature>Tests.cs. - For full-cycle API tests, use controller-focused structure: one fixture per controller/test family and one base
ApiTest.cs+ApiFixture.cs(split by scenario family only when needed). - Do not translate SpecFlow/Taffy step definitions into C# line-by-line; rewrite scenario intent into idiomatic API tests.
- For API migrations, avoid one global shared setup fixture; each controller/test family fixture owns its own dependencies.
- Fixture ownership for API tests should include DB launcher + migrators + WireMock + WebApplicationFactory + client.
- Reset mutable state in
[SetUp]; dispose all owned infra in[OneTimeTearDown]. - For DB bootstrapping, follow pricing-style
DatabaseLauncher + MigratorContainerfromtests/utils/Sc.Fin.Pricing.Tests.Utils/Testcontainers. - Use canonical migrator command
dotnet Sc.Tool.FluentMigrator.dll migrateup -m /sqland avoid custom ready-check arguments in tests. - Keep migrator ordering explicit (dependency migrators first, domain migrator last) and support fixture-level optional migrator toggles when some suites do not need all DBs.
- Add explicit migrator verification tests that assert launcher startup, migrator completion/order, and required tables.
- Use iterative quality loop:
code -> build -> run tests -> fix -> repeat. - For health endpoints, use
[Test] + [TestCase] + [CancelAfter(...)]with method signature(string url, CancellationToken cancellationToken); keep[Test]together with[TestCase]to avoid NUnit analyzer issues. - If user excludes infra-dependent suites (for example component tests requiring Docker), run feasible categories first and report exactly what remains unvalidated.
- If the task shifts into service design or backend refactoring, switch to
$software-csharp-backend. - If the task shifts into
nuke/Build.cs, category target wiring, or CI artifact publication, switch to$ops-nuke-cicd.
Workflow
1. Define boundary, dependencies, expected assertion depth, and environment constraints. Load references/nunit-structure.md. 2. Select fixture composition and lifecycle. Load references/fixture-pattern.md and references/testing-templates.md. 3. Implement scenario tests for the target layer. Load references/api-testing-nunit.md or references/component-testing-nunit.md. 4. Choose double vs real dependency strategy. Load references/dependency-strategy-matrix.md, then references/wiremock-setup.md or references/testcontainers-setup.md. 5. Add resilient async and eventual-consistency assertions. Load references/async-eventual-assertions.md. 6. Harden suite against flaky behavior. Load references/anti-flakiness.md. 7. Tune execution in CI. Load references/ci-parallelism-sharding.md and references/infrastructure-troubleshooting.md. 8. Validate changed suites through build-test feedback targets. For NUKE-based repositories, run BuildAll, LocalUnitTest, ApiTest/DbTest as needed, then TestAll; use $ops-nuke-cicd for pipeline-target changes. 9. If this is a migration from SpecFlow-style assets, produce migration trace artifacts. Use $docs-codebase with migration matrix and feature trace templates.
Resources
- NUnit Structure: project layout, naming, categories, and lifecycle conventions.
- Fixture Pattern: fixture boundaries, shared setup, teardown, and composition.
- Testing Templates: copy-ready fixture/Testcontainers/WireMock templates.
- API Testing with NUnit: endpoint-level tests with HTTP assertions and contract checks.
- Component Testing with NUnit: in-process integration tests across collaborating components.
- Dependency Strategy Matrix: decide WireMock vs Testcontainers by scenario.
- WireMock Setup: deterministic stubs, request verification, and failure simulation.
- Testcontainers Setup: container lifecycle, readiness, and test isolation.
- Async Eventual Assertions: polling, timeouts, and message-driven verification.
- Anti-Flakiness: reliability rules for stable execution.
- CI Parallelism and Sharding: split test execution safely and efficiently.
- Infrastructure Troubleshooting: diagnose startup failures, port collisions, and readiness issues.
- Skill Data: curated Microsoft and .NET testing references for this skill.
Templates
- NUnit Handler Fixture Template: base fixture for setup wiring and deterministic scenario configuration.
- NUnit Handler Tests Template: base test class using fixture with Arrange/Act/Assert flow.
- NUnit API Fixture Template: API fixture for controller-focused API-to-database full-cycle tests.
- NUnit API Tests Template: base API test class with fixture isolation and parallel-safe lifecycle.
- NUnit API Request Builder Template: deterministic request builder for scenario setup.
- NUnit API TestCaseSources Template: reusable
TestCaseDatasource methods. - NUnit WireMock Template: pricing-style
WireMockServerWrapperand per-dependency*WiremockServerhelper pattern. - NUnit Database Launcher Template: pricing-style
DatabaseLauncher, ordered migrator chain, optional migrator toggles, and startup verification hooks.
interface:
display_name: "NUnit Testing"
short_description: "Build resilient NUnit test suites"
default_prompt: "Use $qa-testing-nunit to design API, component, and integration NUnit suites with fixtures, WireMock, Testcontainers, and anti-flaky patterns."
using Microsoft.Extensions.DependencyInjection;
namespace Company.Product.Tests.Api;
internal sealed partial class TransactionControllerApiFixture : IAsyncDisposable
{
private readonly TransactionApiFixtureRuntime _runtime = new();
internal ITransactionsApiClient ApiClient => _runtime.ApiClient;
internal Task InitializeAsync() => _runtime.InitializeAsync();
internal Task ResetAsync() => _runtime.ResetAsync();
public ValueTask DisposeAsync() => _runtime.DisposeAsync();
internal async Task<TransactionControllerApiFixture> GivenTransactionExistsAsync(PaymentTransactionDto transaction)
{
await _runtime.WithScopeAsync(async scope =>
{
var database = scope.ServiceProvider.GetRequiredService<IMongoDatabase>();
var transactions = database.GetCollection<PaymentTransactionDto>("paymentTransactions");
await transactions.InsertOneAsync(transaction);
});
return this;
}
internal async Task<TransactionControllerApiFixture> GivenOutboundTransferCanBeCreatedAsync()
{
await _runtime.WithWireMockAsync(server =>
{
var cardTransferWiremockServer = new CardTransferWiremockServer(server, Guid.NewGuid());
cardTransferWiremockServer.GivenOutboundTransferCanBeCreated(new object());
});
return this;
}
}
internal sealed class TransactionApiFixtureRuntime : IAsyncDisposable
{
private DatabaseLauncher? _databaseLauncher;
private WireMockServerWrapper? _wireMockServer;
private CustomWebApplicationFactory? _factory;
private IServiceScope? _scope;
internal ITransactionsApiClient ApiClient { get; private set; } = null!;
internal async Task InitializeAsync()
{
_databaseLauncher = new DatabaseLauncher();
_ = await _databaseLauncher.LaunchAsync();
_wireMockServer = new WireMockServerWrapper();
_wireMockServer.Start();
_factory = new CustomWebApplicationFactory(_wireMockServer.Url);
var httpClient = _factory.CreateClient();
ApiClient = RestService.For<ITransactionsApiClient>(httpClient);
_scope = _factory.Services.CreateScope();
await ResetAsync();
}
internal async Task ResetAsync()
{
if (_wireMockServer is not null)
{
_wireMockServer.Reset();
}
if (_scope is not null)
{
await CleanupStateAsync(_scope.ServiceProvider);
}
}
internal Task WithScopeAsync(Func<IServiceScope, Task> action)
{
if (_scope is null)
{
throw new InvalidOperationException("Fixture runtime is not initialized.");
}
return action(_scope);
}
internal Task WithWireMockAsync(Action<WireMockServerWrapper> action)
{
if (_wireMockServer is null)
{
throw new InvalidOperationException("WireMock server is not initialized.");
}
action(_wireMockServer);
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
_scope?.Dispose();
if (_factory is not null)
{
await _factory.DisposeAsync();
}
_wireMockServer?.Dispose();
if (_databaseLauncher is not null)
{
await _databaseLauncher.DisposeAsync();
}
}
private static Task CleanupStateAsync(IServiceProvider serviceProvider)
{
// Replace with DB cleanup for your storage.
_ = serviceProvider;
return Task.CompletedTask;
}
}
internal interface ITransactionsApiClient;
internal interface IMongoDatabase
{
IMongoCollection<TDocument> GetCollection<TDocument>(string name);
}
internal interface IMongoCollection<T>
{
Task InsertOneAsync(T entity);
}
internal sealed class PaymentTransactionDto;
internal sealed class WireMockServerWrapper : IDisposable
{
internal string Url => "http://127.0.0.1:8080";
internal void Start() { }
internal void Reset() { }
public void Dispose() { }
}
internal sealed class CardTransferWiremockServer
{
internal CardTransferWiremockServer(WireMockServerWrapper wireMockServer, Guid userId)
{
_ = wireMockServer;
_ = userId;
}
internal void GivenOutboundTransferCanBeCreated(object request) => _ = request;
}
internal sealed class CustomWebApplicationFactory : IAsyncDisposable
{
internal CustomWebApplicationFactory(string wireMockUrl) => _ = wireMockUrl;
internal IServiceProvider Services { get; } = new ServiceCollection().BuildServiceProvider();
internal HttpClient CreateClient() => new();
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
}
internal static class RestService
{
internal static T For<T>(HttpClient client) where T : class
{
_ = client;
throw new NotImplementedException();
}
}
using Bogus;
namespace Company.Product.Tests.Api;
internal sealed class TransactionRequestBuilder
{
private readonly Faker<CreatePaymentTransactionRequest> _targetFaker;
private Faker<TransactionDetailsRequest> _detailsFaker;
private Faker<CurrencyConversionRequest>? _conversionFaker;
internal TransactionRequestBuilder()
{
_targetFaker = new Faker<CreatePaymentTransactionRequest>();
_detailsFaker = new Faker<TransactionDetailsRequest>()
.RuleFor(x => x.Currency, _ => "USD");
}
internal TransactionRequestBuilder WithParties(IList<Party> parties)
{
_targetFaker.RuleFor(x => x.ParticipatingParties, parties);
return this;
}
internal TransactionRequestBuilder WithCurrency(string currency)
{
_detailsFaker.RuleFor(x => x.Currency, currency);
return this;
}
internal TransactionRequestBuilder WithCurrencyConversion(string receivingCurrency)
{
_conversionFaker = new Faker<CurrencyConversionRequest>()
.RuleFor(x => x.CurrencyRateHash, Guid.NewGuid().ToString("D"))
.RuleFor(x => x.ReceivingCurrency, receivingCurrency);
return this;
}
internal TransactionRequestBuilder WithoutCurrencyConversion()
{
_conversionFaker = null;
return this;
}
internal CreatePaymentTransactionRequest Build()
{
return _targetFaker
.RuleFor(x => x.TransactionDetails, _detailsFaker.Generate())
.RuleFor(x => x.CurrencyConversion, _conversionFaker?.Generate())
.Generate();
}
}
internal sealed class CreatePaymentTransactionRequest
{
public IList<Party> ParticipatingParties { get; set; } = [];
public TransactionDetailsRequest TransactionDetails { get; set; } = new();
public CurrencyConversionRequest? CurrencyConversion { get; set; }
}
internal sealed class TransactionDetailsRequest
{
public string Currency { get; set; } = string.Empty;
}
internal sealed class CurrencyConversionRequest
{
public string CurrencyRateHash { get; set; } = string.Empty;
public string ReceivingCurrency { get; set; } = string.Empty;
}
internal sealed class Party;
namespace Company.Product.Tests.Api;
internal static class TransactionApiTestCaseSources
{
internal static IEnumerable<TestCaseData> ValidPayouts()
{
yield return new TestCaseData(PayoutFakers.CardPayoutFaker().Generate());
yield return new TestCaseData(PayoutFakers.CryptoPayoutFaker().Generate());
yield return new TestCaseData(PayoutFakers.WalletPayoutFaker().Generate());
}
internal static IEnumerable<TestCaseData> SupportedCurrencies()
{
yield return new TestCaseData("USD");
yield return new TestCaseData("EUR");
yield return new TestCaseData("GBP");
}
}
internal static class PayoutFakers
{
internal static Bogus.Faker<object> CardPayoutFaker() => new Bogus.Faker<object>();
internal static Bogus.Faker<object> CryptoPayoutFaker() => new Bogus.Faker<object>();
internal static Bogus.Faker<object> WalletPayoutFaker() => new Bogus.Faker<object>();
}
using NUnit.Framework;
[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]
namespace Company.Product.Tests.Api;
[Category("ApiTest")]
[TestFixture]
[Parallelizable]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
internal sealed partial class TransactionControllerApiTest
{
private static readonly TransactionControllerApiFixture Fixture = new();
[OneTimeSetUp]
public static Task OneTimeSetUp() => Fixture.InitializeAsync();
[OneTimeTearDown]
public static async Task OneTimeTearDown() => await Fixture.DisposeAsync();
[SetUp]
public Task SetUp() => Fixture.ResetAsync();
[Test]
public async Task Should_Create_Transaction_When_Request_Is_Valid()
{
// Arrange
var request = new CreateTransactionRequest();
// Act
var response = await Fixture.ApiClient.CreateAsync(request, CancellationToken.None);
// Assert
Assert.That(response.StatusCode, Is.EqualTo(201));
}
}
internal sealed class CreateTransactionRequest;
internal sealed class ApiResponse
{
internal int StatusCode { get; init; }
}
internal interface ITransactionsApiClient
{
Task<ApiResponse> CreateAsync(CreateTransactionRequest request, CancellationToken cancellationToken);
}
using DotNet.Testcontainers.Builders;
using DotNet.Testcontainers.Networks;
using Microsoft.Data.SqlClient;
using Company.Product.Tests.Utils.Testcontainers.Migrator;
using Testcontainers.MsSql;
public sealed class DatabaseLaunchOptions
{
public bool RunAuxiliaryMigrator { get; init; } = true;
}
public sealed class DatabaseLaunchResult
{
public required MsSqlContainer Container { get; init; }
public required string MainConnectionString { get; init; }
public required string AuxiliaryConnectionString { get; init; }
public required IReadOnlyCollection<string> MigratorExecutionOrder { get; init; }
}
public sealed class DatabaseLauncher : IAsyncDisposable
{
private const string SqlServerAlias = "sql-db";
private const string MainDatabaseName = "service_main";
private const string AuxiliaryDatabaseName = "service_aux";
private const string MainMigratorImage = "reg.corp.swiftcom.uk/sc.tool/sc_tool_fluentmigrator:latest";
private const string DependencyMigratorImage = "reg.corp.swiftcom.uk/company/dependency-migrator:latest";
private const string AuxiliaryMigratorImage = "reg.corp.swiftcom.uk/company/aux-migrator:latest";
private readonly INetwork _network = new NetworkBuilder()
.WithName("api_tests_network_" + Guid.NewGuid().ToString("N"))
.WithDriver(NetworkDriver.Bridge)
.WithCleanUp(true)
.Build();
private readonly List<MigratorContainer> _externalMigrators = [];
private readonly List<string> _executionOrder = [];
private MsSqlContainer? _sqlContainer;
private MigratorContainer? _mainMigrator;
public async Task<DatabaseLaunchResult> LaunchAsync(
DatabaseLaunchOptions? options = null,
CancellationToken cancellationToken = default)
{
options ??= new DatabaseLaunchOptions();
await _network.CreateAsync(cancellationToken);
_sqlContainer = MsSqlContainers.Create(_network, SqlServerAlias);
await _sqlContainer.StartAsync(cancellationToken);
await EnsureDatabaseExistsAsync(_sqlContainer.GetConnectionString(), MainDatabaseName, cancellationToken);
await EnsureDatabaseExistsAsync(_sqlContainer.GetConnectionString(), AuxiliaryDatabaseName, cancellationToken);
var mainConnectionString = BuildConnectionString(_sqlContainer.GetConnectionString(), MainDatabaseName, SqlServerAlias);
var auxiliaryConnectionString = BuildConnectionString(_sqlContainer.GetConnectionString(), AuxiliaryDatabaseName, SqlServerAlias);
await RunExternalMigratorAsync("dependency", DependencyMigratorImage, mainConnectionString, cancellationToken);
await VerifyTableExistsAsync(mainConnectionString, "dbo", "RequiredDependencyTable", cancellationToken);
if (options.RunAuxiliaryMigrator)
{
await RunExternalMigratorAsync("auxiliary", AuxiliaryMigratorImage, auxiliaryConnectionString, cancellationToken);
await VerifyTableExistsAsync(auxiliaryConnectionString, "dbo", "AuxiliaryState", cancellationToken);
}
await RunMainMigratorAsync(mainConnectionString, ResolveMigrationsFolder(), cancellationToken);
return new DatabaseLaunchResult
{
Container = _sqlContainer,
MainConnectionString = mainConnectionString,
AuxiliaryConnectionString = auxiliaryConnectionString,
MigratorExecutionOrder = _executionOrder.ToArray()
};
}
public async ValueTask DisposeAsync()
{
if (_mainMigrator is not null)
{
await _mainMigrator.DisposeAsync();
}
foreach (var migrator in _externalMigrators)
{
await migrator.DisposeAsync();
}
if (_sqlContainer is not null)
{
await _sqlContainer.DisposeAsync();
}
await _network.DisposeAsync();
GC.SuppressFinalize(this);
}
private async Task RunExternalMigratorAsync(string name, string image, string connectionString, CancellationToken cancellationToken)
{
var migrator = MigratorContainers.CreateExternalMigrator(name, image, _network, connectionString);
_externalMigrators.Add(migrator);
await migrator.StartAsync(cancellationToken);
_executionOrder.Add(name);
}
private async Task RunMainMigratorAsync(string connectionString, string migrationsFolder, CancellationToken cancellationToken)
{
_mainMigrator = MigratorContainers.CreateMainMigrator(MainMigratorImage, _network, connectionString, migrationsFolder);
await _mainMigrator.StartAsync(cancellationToken);
_executionOrder.Add("main");
}
private static string ResolveMigrationsFolder()
{
return Path.GetFullPath("../../../../../../db/migrations");
}
private static string BuildConnectionString(string sourceConnectionString, string databaseName, string sqlServerAlias)
{
var builder = new SqlConnectionStringBuilder(sourceConnectionString)
{
InitialCatalog = databaseName,
DataSource = sqlServerAlias,
Encrypt = false,
TrustServerCertificate = true
};
return builder.ConnectionString;
}
private static async Task EnsureDatabaseExistsAsync(string sourceConnectionString, string databaseName, CancellationToken cancellationToken)
{
var builder = new SqlConnectionStringBuilder(sourceConnectionString)
{
InitialCatalog = "master",
Encrypt = false,
TrustServerCertificate = true
};
await using var connection = new SqlConnection(builder.ConnectionString);
await connection.OpenAsync(cancellationToken);
const string sql = "IF DB_ID(@dbName) IS NULL EXEC('CREATE DATABASE [' + @dbName + ']')";
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@dbName", databaseName);
await command.ExecuteNonQueryAsync(cancellationToken);
}
private static async Task VerifyTableExistsAsync(string connectionString, string schema, string table, CancellationToken cancellationToken)
{
const string sql = "SELECT COUNT(1) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=@schema AND TABLE_NAME=@table";
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync(cancellationToken);
await using var command = new SqlCommand(sql, connection);
command.Parameters.AddWithValue("@schema", schema);
command.Parameters.AddWithValue("@table", table);
var exists = Convert.ToInt32(await command.ExecuteScalarAsync(cancellationToken)) > 0;
if (!exists)
{
throw new InvalidOperationException($"Expected table '{schema}.{table}' is missing after migrator execution.");
}
}
}
public static class MigratorContainers
{
private static readonly string[] ExternalMigratorCommand = ["migrateup", "-m", "../sql"];
public static MigratorContainer CreateExternalMigrator(string name, string image, INetwork network, string connectionString)
{
return new MigratorContainerBuilder()
.WithImage(image)
.WithName($"{name}_{Guid.NewGuid():N}")
.WithNetwork(network)
.WithDatabaseType(DatabaseType.SqlServer)
.WithConnectionString(connectionString)
.WithDatabaseSchema("dbo")
.WithCommand(ExternalMigratorCommand)
.Build();
}
public static MigratorContainer CreateMainMigrator(string image, INetwork network, string connectionString, string migrationsFolder)
{
return new MigratorContainerBuilder()
.WithImage(image)
.WithName("main_migrator_" + Guid.NewGuid().ToString("N"))
.WithNetwork(network)
.WithDatabaseType(DatabaseType.SqlServer)
.WithConnectionString(connectionString)
.WithDatabaseSchema("dbo")
.WithMigrationsFolder(migrationsFolder)
.WithDefaultMigratorCommand()
.Build();
}
}
using FluentResults;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
namespace Company.Product.Tests.Handlers;
internal sealed partial class CreateEntityCommandHandlerFixture
{
private readonly IMediator _mediator;
internal CreateEntityCommand Command { get; private set; } = null!;
internal CreateEntityCommandHandlerFixture(IServiceProvider services)
{
_mediator = services.GetRequiredService<IMediator>();
}
internal CreateEntityCommandHandlerFixture GivenCommand(CreateEntityCommand command)
{
Command = command;
return this;
}
internal CreateEntityCommandHandlerFixture GivenValidationPassed()
{
// Configure mocks/stubs for success path.
return this;
}
internal CreateEntityCommandHandlerFixture GivenDependencyFailure()
{
// Configure mocks/stubs for failure path.
return this;
}
internal Task<Result<CreateEntityResult>> SendAsync(CreateEntityCommand command)
=> _mediator.Send(command, CancellationToken.None);
}
internal sealed record CreateEntityCommand(Guid UserId) : IRequest<Result<CreateEntityResult>>;
internal sealed record CreateEntityResult(Guid EntityId);
using AwesomeAssertions;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
namespace Company.Product.Tests.Handlers;
[Parallelizable]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
[TestOf(typeof(CreateEntityCommandHandler))]
internal sealed partial class CreateEntityCommandHandlerTests
{
private CreateEntityCommandHandlerFixture _fixture = null!;
[SetUp]
public Task SetUp()
{
IServiceProvider services = BuildServices();
_fixture = new CreateEntityCommandHandlerFixture(services);
return Task.CompletedTask;
}
[Test]
public async Task Should_Create_Entity_When_Request_Is_Valid()
{
// Arrange
var command = new CreateEntityCommand(Guid.NewGuid());
_fixture.GivenCommand(command)
.GivenValidationPassed();
// Act
var result = await _fixture.SendAsync(command);
// Assert
result.Should().BeSuccessful();
}
private static IServiceProvider BuildServices()
{
var services = new ServiceCollection();
// Register handler and test doubles here.
return services.BuildServiceProvider();
}
}
internal sealed class CreateEntityCommandHandler;
using Newtonsoft.Json;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Settings;
public sealed class WireMockServerWrapper : IDisposable
{
private WireMockServer _wireMockServer = null!;
public WireMockServer Server => _wireMockServer;
public string Url => _wireMockServer.Url!;
public void Start()
{
Start(new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include
});
}
public void Start(JsonSerializerSettings jsonSerializerSettings)
{
_wireMockServer = WireMockServer.Start(new WireMockServerSettings
{
StartAdminInterface = true,
JsonSerializerSettings = jsonSerializerSettings
});
}
public void Reset() => _wireMockServer.Reset();
public void Dispose() => _wireMockServer.Dispose();
}
public sealed class DependencyWiremockServer(WireMockServerWrapper serverWrapper)
{
private readonly WireMockServer _wireMockServer = serverWrapper.Server;
public void GivenSuccess()
{
_wireMockServer
.Given(Request.Create()
.UsingMethod("GET")
.WithPath("/v1/resource"))
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson(_ => new { ok = true }));
}
}
{
"topic": "dotnet_testing_feedback_loop",
"microsoft_references": [
{
"title": ".NET Testing Overview",
"url": "https://learn.microsoft.com/en-us/dotnet/core/testing/"
},
{
"title": "dotnet test CLI",
"url": "https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test"
},
{
"title": ".NET Code Coverage",
"url": "https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage"
},
{
"title": "ASP.NET Core Integration Tests",
"url": "https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-9.0"
},
{
"title": "C# Coding Conventions",
"url": "https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions"
},
{
"title": ".NET Source Code",
"url": "https://github.com/dotnet"
}
]
}
Anti-Flakiness
Purpose
Use this guide to eliminate nondeterminism in NUnit API/component/integration tests.
Determinism Rules
- Freeze time or inject clock abstraction.
- Use fixed random seeds or deterministic data builders.
- Avoid real network calls outside controlled test doubles/containers.
Concurrency and Timing
- Replace arbitrary sleeps with polling + timeout.
- Set explicit timeouts for async operations.
- Keep retry policies in tests intentional and bounded.
Isolation
- Reset shared state between tests.
- Use unique identifiers per test run.
- Prevent static mutable state bleed.
Diagnostics
- Log correlation IDs and request/response bodies on failure.
- Persist container and WireMock logs for failed tests.
- Include fixture startup and teardown timing in failure output.
Port Collision Regression
- Validate that tests still pass when previously hard-coded ports are intentionally occupied. This confirms the suite is truly decoupled from fixed port assumptions.
- Treat port collision as a first-class regression scenario when migrating from hard-coded to dynamic port allocation.
CI Stability Checklist
- Cap parallelism if shared resources are contested.
- Mark known long-running categories separately.
- Ensure cleanup runs even when setup partially fails.
API Testing with NUnit
Purpose
Use this guide for black-box or near-black-box API tests.
Test Scope
- Boot the API host with production-like middleware.
- Exercise endpoints through HTTP client, not controller internals.
- Validate both transport contract and domain effect.
Full-Cycle Pattern (API -> DB)
1. Start infra in [OneTimeSetUp]:
- Testcontainers for owned stateful dependencies (DB, broker).
- WireMock for external HTTP dependencies.
- test host factory wired to those dependency endpoints.
2. In [SetUp], create HTTP client + typed API client and construct per-test fixture. 3. Arrange via fixture Given... helpers and request builders. 4. Execute API call through client. 5. Assert transport contract and persisted state/side effects. 6. In [TearDown], dispose per-test fixture and HTTP client. 7. In [OneTimeTearDown], stop and dispose all infra deterministically.
Migration Guidance (SpecFlow/Taffy -> NUnit)
- Rewrite scenarios into controller-focused API tests.
- Preserve scenario parity, but avoid step-by-step translation of legacy Given/When/Then files.
- Keep test intent in assertions, not in copied step text.
- Add explicit migration parity artifacts (matrix + per-feature trace).
Core Pattern
1. Arrange request payload and prerequisite state. 2. Send HTTP request through configured client. 3. Assert status code and response body contract. 4. Verify persistence/event side effects when relevant.
What to Assert
- Expected status code and media type.
- Required fields in success and error payloads.
- Contract details for
ProblemDetailsfailures. - Idempotency and conflict behavior where applicable.
Coverage Baseline
- Happy path.
- Validation failure.
- Domain/business-rule failure.
- AuthN/AuthZ failure when endpoint is protected.
- Dependency failure translation when upstream calls are involved.
- Idempotency checks for repeated operations (same transaction id / same confirmation id / same payout id).
- Concurrency conflict checks (for example repository version conflict to
409 Conflict).
Mandatory Migration Verification Add-on
- Add one dedicated migrator verification test suite that checks:
- DB launcher starts correctly,
- migrators run in expected order,
- required schema tables exist before API tests execute.
Async Eventual Assertions
Purpose
Use this guide for message-driven or eventually consistent flows.
Polling Pattern
- Poll on a bounded interval.
- Stop immediately when condition matches.
- Fail with clear timeout diagnostics.
Template
public static async Task Eventually(
Func<Task<bool>> condition,
TimeSpan timeout,
TimeSpan interval)
{
DateTimeOffset deadline = DateTimeOffset.UtcNow + timeout;
while (DateTimeOffset.UtcNow < deadline)
{
if (await condition())
{
return;
}
await Task.Delay(interval);
}
Assert.Fail($"Condition not met within {timeout}.");
}Rules
- Avoid
Thread.Sleepin tests. - Keep timeout and interval explicit per scenario.
- Log key ids and state snapshots on timeout.
CI Parallelism and Sharding
Purpose
Use this guide to scale NUnit execution while keeping shared resources stable.
Parallelism Rules
- Run unit tests with high parallelism.
- Limit API/component/integration parallelism when infrastructure is shared.
- Separate contention-heavy categories from fast categories.
- Prefer fixture-level parallelism for API suites that use isolated per-fixture dependencies.
Fixture Isolation Preconditions
- Use one fixture per controller/test family.
- Ensure each fixture owns independent runtime dependencies.
- Reset mutable state in
[SetUp]. - Dispose owned runtime in
[OneTimeTearDown].
NUnit Parallel Baseline for API Suites
- Assembly:
ParallelScope.Fixtureswith explicit level of parallelism. - Test classes:
[Parallelizable]+[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]. - Test category:
[Category("ApiTest")].
Sharding Strategy
- Split by category first (
ApiTest,ComponentTests,DbTests). - Then shard by assembly or namespace.
- Keep shard duration balanced with historical timing.
Guardrails
- Use unique identifiers for test data per shard.
- Reserve fixed ports only when unavoidable.
- Always collect container and test logs for failed shards.
- Avoid running multiple
dotnet testcommands in parallel against the same project output path.
Component Testing with NUnit
Purpose
Use this guide for in-process tests that validate collaboration between multiple components.
Boundaries
- Include real composition root for the targeted module.
- Replace only out-of-process dependencies when needed.
- Keep database/message broker real when behavior depends on integration semantics.
Pattern
1. Build service provider with test configuration. 2. Seed state using deterministic builders. 3. Execute use case through public component API. 4. Assert output plus state transitions.
API-Component Hybrid Pattern
- Use this when tests run through HTTP but still verify database and message side effects.
- Keep web app host real and infrastructure production-like for owned dependencies.
- Use WireMock for external services and Testcontainers for owned stateful services.
- Keep per-test fixture object as orchestration layer for Given/When helper methods.
- Keep fixture scope aligned to one controller/test family for migration and parallel execution.
Controller-Focused Migration Rule
- When migrating from feature-file grouping, move to controller-focused files and fixtures.
- Keep one fixture per controller/test family and avoid shared global setup fixture for all controllers.
Guidance
- Prefer component tests for handler + repository + mapper behavior.
- Keep each test focused on one end-to-end component scenario.
- Avoid asserting private implementation details.
Exit Criteria
- Component tests catch wiring issues that unit tests miss.
- Test runtime remains acceptable by limiting permutations.
Dependency Strategy Matrix
Purpose
Use this matrix to choose WireMock or Testcontainers per dependency and test goal.
Decision Matrix
- Need strict external API contract simulation with deterministic payloads: Use WireMock.
- Need database transaction semantics, query behavior, or migration checks: Use Testcontainers.
- Need broker behavior (ack, retry, ordering): Use Testcontainers.
- Need fast negative-path coverage for upstream HTTP failures: Use WireMock.
- Need confidence in production-like integration behavior: Prefer Testcontainers.
Combined Strategy
- Use WireMock for third-party HTTP APIs.
- Use Testcontainers for owned stateful infrastructure.
- Keep this split explicit in fixture setup comments.
- For API full-cycle tests, this combined strategy is the default baseline.
Fixture Pattern
Purpose
Use this guide when building reusable setup for NUnit API/component/integration tests.
Fixture Boundaries
- Keep fixture scope equal to resource lifetime.
- Avoid one global fixture for unrelated scenarios.
- Share only immutable configuration and expensive dependencies.
- Keep default fixture focused on one handler/use case or one controller/test family.
Per-Controller/Test-Family Model (API Migrations)
- Use one fixture per controller/test family.
- Each fixture owns required runtime dependencies:
- database launcher,
- migrators,
- wiremock server wrappers,
- web application factory,
- HTTP/typed clients.
- Reset mutable state in
[SetUp]. - Dispose all owned dependencies in
[OneTimeTearDown]. - Do not introduce a single global shared setup fixture across unrelated controllers.
Recommended Pattern
1. Start with one fixture file per use case (<Feature>Fixture.cs) or controller (<Controller>ApiFixture.cs). 2. Build resource factory methods and Given... scenario setup methods in fixture class. 3. Start dependencies in [OneTimeSetUp] only when reuse is safe. 4. Reset mutable state in [SetUp] before each test. 5. Dispose resources in [OneTimeTearDown] with defensive cleanup.
Two-File Baseline
- Pair fixture with one base test file (
<Feature>Tests.csor<Controller>ApiTest.cs). - Keep fixture fluent and scenario-oriented:
GivenCommand(...)GivenRoutingCalculated()GivenValidationPassed()Send(...)- Return fixture instance from setup methods to keep Arrange phase linear.
API Full-Cycle Fixture Pattern
- For API-to-database tests, use one base fixture (
<Controller>ApiFixture.cs) that: - wraps API client calls,
- manages DB verification helpers,
- keeps scenario setup close to test intent.
- Create fixture inside
[SetUp]and dispose in[TearDown]to avoid state leakage. - Keep expensive infra (containers, network, wiremock host, web app factory) at controller fixture scope, not repository-global scope.
- Keep builder helpers (
GivenRequest(),GivenEntity()) in fixture or dedicated builder files.
Composition Rules
- Prefer small fixtures combined by helper methods.
- Keep test data builders outside fixture lifecycle logic.
- Hide infrastructure wiring behind clear fixture API.
- When scenario count becomes large, split fixture into partial files by scenario family while keeping one shared base fixture file.
Failure Hygiene
- Capture startup logs when fixture init fails.
- Fail fast on missing ports/connection strings.
- Never swallow teardown exceptions silently.
Infrastructure Troubleshooting
Purpose
Use this guide to diagnose common test infrastructure failures.
Startup Failures
- Verify container image tags are valid and pinned.
- Check readiness timeout vs actual startup duration.
- Print container logs before failing fixture setup.
Port Collisions
- Prefer dynamic ports for local and CI runs.
- Expose chosen port in diagnostics.
- Avoid global static server instances across fixtures.
Readiness Issues
- Wait on health endpoint or explicit query, not startup completion alone.
- Re-check connection strings from runtime configuration.
- Validate migration/seed step completed before first assertion.
Migrator Ordering Issues
- Symptom:
Cannot find the object 'dbo.TPurseSections'. - Cause: dependent migrator did not run before ledger/service migrator.
- Fix: enforce deterministic migrator order and table checks between steps.
- Symptom: migrator exits but schema is incomplete.
- Cause: wrong command/entrypoint or wrong mounted migration folder.
- Fix: use canonical migrator command and verify bind mounts.
Command Mismatch Issues
- Symptom: migrator behavior differs from reference repos.
- Cause: custom command flags (for example
--readycheck) diverge from baseline. - Fix: use default fluent migrator command
dotnet Sc.Tool.FluentMigrator.dll migrateup -m /sqlunless explicitly required otherwise.
Cleanup Issues
- Dispose containers and servers in
OneTimeTearDown. - Guard teardown with null checks for partial setup failures.
- Surface teardown errors in test output.
Build/Test Concurrency Issues
- Symptom: intermittent MSBuild/test failures with locked files.
- Cause: parallel
dotnet testruns against the same project output path. - Fix: run project-level test invocations sequentially or isolate output paths.
Prerequisite Gaps
- If Docker/Testcontainers is unavailable, do not run container-dependent suites implicitly.
- Run feasible suites first (unit or non-container API tests).
- Report skipped categories explicitly with reason and follow-up command.
- Keep final validation summary clear about what is verified vs pending.
NUnit Structure
Purpose
Use this guide to define predictable structure for NUnit-based test suites.
Project Layout
- Mirror production modules in
tests/to keep ownership clear. - Separate fast unit tests from slower API/component tests.
- Group shared helpers under a dedicated utility namespace.
Naming
- Name files
*Tests.cs. - Name fixture/setup helpers
*Fixture.cs. - Name tests as behavior statements:
Should_<Result>_When_<Condition>.
File Pattern
- Start with two files for each handler/use case:
<Feature>Fixture.cs: dependency wiring + deterministic Given helpers.<Feature>Tests.cs: NUnit lifecycle and test methods.- Use
partialclasses for both fixture and tests when scenario matrix grows by route/provider/product variant. - Keep one base
Tests.cs+ one baseFixture.csand extend with variant-specific partial files only as needed.
API Full-Cycle Variant
- Use controller-focused structure as default migration target:
- one fixture per controller/test family,
- one base controller test class,
- optional scenario split files (
.Positive.cs,.Negative.cs,.Validation.cs,.Query.cs,.Refund.cs). - Keep one base API fixture file (
<Controller>ApiFixture.cs) for: - scenario helper methods (
Given...), - repository checks,
- API client helper methods.
- Keep fixture and test file names aligned to controller scope, not legacy feature-file scope.
SpecFlow Migration Layout Rules
- Do not preserve legacy feature grouping when it conflicts with controller boundaries.
- Keep behavior parity in test methods, but rewrite structure into controller-focused suites.
- Add migration trace artifacts documenting scenario-to-test mapping and fixture ownership.
Categories
- Apply categories consistently (
ApiTest,ComponentTests,DbTests). - Keep category usage aligned with build filters.
Lifecycle Conventions
- Use
[SetUp]for per-test initialization. - Use
[OneTimeSetUp]only for expensive shared resources within one fixture scope. - Keep teardown explicit and idempotent.
- For API tests, combine
[Parallelizable]+[FixtureLifeCycle(LifeCycle.InstancePerTestCase)].
Assertion Style
- Assert one behavior per test.
- Verify status/result first, then payload/side effects.
- Prefer expressive assertions over chained manual checks.
Testcontainers Setup
Purpose
Use this guide for ephemeral infrastructure in integration and component tests.
Container Lifecycle
- Create container definitions with fixed image tags.
- Start containers before executing tests.
- Wait for readiness via health check or explicit probe.
- Dispose containers reliably after test execution.
- For API full-cycle suites, keep containers static and start them once in
[OneTimeSetUp]. - Create required schema/collections/topics during one-time setup after readiness.
- For SQL databases, prefer one-shot migrator containers over ad-hoc schema SQL in test code.
Pricing Pattern: Database Launcher + Migrator
- Keep database startup orchestration in
DatabaseLauncher. - Run DB and migrators in one shared docker network so migrators connect via network alias.
- Use
MigratorContainer+MigratorContainerBuilderfor migrator startup; avoid hand-crafted per-callContainerBuildercode. - Start migrator containers as short-lived jobs and wait for exit code
0. - Mount migration folders into migrator container (
/sql, optional/sql/component-tests). - Keep canonical migrator command:
dotnet Sc.Tool.FluentMigrator.dll migrateup -m /sql. - Do not add custom ready-check parameters in tests; rely on wait strategy + startup timeout.
- Resolve migration paths from repository root in pricing style, for example
Path.GetFullPath("../../../../../../db/"). - Reuse this pricing reference implementation:
tests/utils/Sc.Fin.Pricing.Tests.Utils/Testcontainers.
Ordered Migrator Chain (SQL Server Migration Cases)
- Run dependency migrators first.
- Run service/domain migrator last.
- Validate required tables after each critical step.
- Add fixture-level launch options when some suites do not require all migrators/DBs.
Template
- Use
assets/nunit-database-launcher-template.csas the starting point for: DatabaseLauncher<Db>ContainersMigratorContainersMigratorContainerBuilder+ exit-code wait strategy- optional migrator launch options and verification hooks
Isolation Rules
- Prefer per-fixture containers for expensive dependencies.
- Use unique database/schema/topic names per test where shared container is used.
- Avoid cross-test state leakage through explicit cleanup.
- Dispose per-test scopes/clients in
[TearDown]even when containers are shared across all tests.
Configuration
- Inject container connection details through test host configuration.
- Keep startup timeout explicit and environment-aware.
- Emit startup logs on readiness failure.
- When using multiple containers (for example DB + broker + schema registry), compose them with one shared test network.
CI Docker Host Resolution
- CI environments cannot assume
localhost. GitLab runners using remote Docker hosts make loopback the wrong advertised endpoint. - Test harnesses must resolve the externally reachable Docker host from Testcontainers host detection or the
DOCKER_HOSTenvironment variable instead of hard-coding127.0.0.1. - Hard-coded localhost ports create a false sense of stability — one port collision breaks the entire suite. Use dynamic host-port reservation and provide environment overrides for local manual runs.
- Code-only fixes can make a suite CI-compatible, but the final proof still requires a real pipeline run.
Common Targets
- Relational databases for persistence behavior.
- Message brokers for async flow verification.
- Caches for TTL/eviction-related behavior.
Testing Templates
Purpose
Use these templates as starting points for NUnit API/component/integration tests.
WireMock reference template: assets/nunit-wiremock-template.cs. Database launcher reference template: assets/nunit-database-launcher-template.cs.
Recommended Default
- Use two files for each handler/use case:
<Feature>Fixture.cs<Feature>Tests.cs- Add extra partial files only when scenario families are large.
Controller-Focused API Migration Default
- Organize API tests around controller/test family, not around legacy feature-file grouping.
- Use one fixture per controller/test family.
- Keep migration parity in test behavior, then document parity in migration trace artifacts.
Fixture Template (<Feature>Fixture.cs)
internal sealed partial class CreatePaymentTransactionHandlerFixture
{
private readonly IMediator _mediator;
private readonly InMemoryPaymentTransactionRepository _repository;
internal CreatePaymentTransactionCommand Command { get; private set; } = null!;
internal CreatePaymentTransactionHandlerFixture(IServiceProvider services)
{
_mediator = services.GetRequiredService<IMediator>();
_repository = services.GetRequiredService<IPaymentTransactionRepository>() as InMemoryPaymentTransactionRepository
?? throw new InvalidOperationException("InMemory repository is required for tests.");
}
internal CreatePaymentTransactionHandlerFixture GivenCommand(CreatePaymentTransactionCommand command)
{
Command = command;
return this;
}
internal CreatePaymentTransactionHandlerFixture GivenValidationPassed()
{
// Setup mocks/stubs here.
return this;
}
internal Task<Result<CreatePaymentTransactionResult>> SendAsync(CreatePaymentTransactionCommand command)
=> _mediator.Send(command, CancellationToken.None);
}Tests Template (<Feature>Tests.cs)
internal sealed partial class CreatePaymentTransactionHandlerTests
{
private static readonly WireMockServerWrapper WireMockServerWrapper = new();
private CreatePaymentTransactionHandlerFixture _fixture = null!;
[OneTimeSetUp]
public static void OneTimeSetUp() => WireMockServerWrapper.Start();
[OneTimeTearDown]
public static void OneTimeTearDown() => WireMockServerWrapper.Stop();
[SetUp]
public Task SetUp()
{
IServiceProvider services = BuildServices();
_fixture = new CreatePaymentTransactionHandlerFixture(services);
return Task.CompletedTask;
}
[Test]
public async Task Should_Create_Transaction_When_Request_Is_Valid()
{
// Arrange
var command = CreatePaymentTransactionCommandBuilder.New().Build();
_fixture.GivenCommand(command)
.GivenValidationPassed();
// Act
var result = await _fixture.SendAsync(command);
// Assert
result.Should().BeSuccessful();
}
}Healthcheck Endpoint Template
[Test]
[TestCase("/health/live")]
[TestCase("/health/startup")]
[TestCase("/health/ready")]
[CancelAfter(10_000)]
public async Task HealthCheck_Should_Return_Healthy(string url, CancellationToken cancellationToken)
{
HttpResponseMessage response;
do
{
response = await PublicApiTestContext.Client.GetAsync(url, cancellationToken);
if (!response.IsSuccessStatusCode)
{
await Task.Delay(100, cancellationToken);
}
}
while (response.StatusCode != HttpStatusCode.OK);
}API Full-Cycle Base Test Template (<Controller>ApiTest.cs)
[assembly: Parallelizable(ParallelScope.Fixtures)]
[Category("ApiTest")]
[TestFixture]
[Parallelizable]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
internal sealed partial class TransactionControllerApiTest
{
private static readonly TransactionControllerApiFixture Fixture = new();
[OneTimeSetUp]
public static Task OneTimeSetUp() => Fixture.InitializeAsync();
[OneTimeTearDown]
public static async Task OneTimeTearDown() => await Fixture.DisposeAsync();
[SetUp]
public Task SetUp() => Fixture.ResetAsync();
}API Full-Cycle Fixture Template (<Controller>ApiFixture.cs)
internal sealed partial class TransactionControllerApiFixture : IAsyncDisposable
{
private readonly TransactionApiFixtureRuntime _runtime = new();
public Task InitializeAsync() => _runtime.InitializeAsync();
public Task ResetAsync() => _runtime.ResetAsync();
public ValueTask DisposeAsync() => _runtime.DisposeAsync();
}Migration Traceability Outputs
- Create matrix mapping old scenario name -> new test method.
- Create per-feature migration trace table with step/block parity status.
- Create controller-focused fixture/test map documenting fixture ownership.
WireMock Setup
Purpose
Use this guide to simulate HTTP dependencies deterministically.
Setup Pattern
- Start one WireMock server per test fixture or per test when isolation requires it.
- Bind stubs to explicit method, path, headers, and body predicates.
- Return deterministic status/body/latency for each scenario.
- For API full-cycle suites, start one wrapper in
[OneTimeSetUp]and stop in[OneTimeTearDown]. - Reconfigure stubs per test through fixture
Given...helper methods. - Use one typed helper class per upstream dependency (
CustomerTariffApiWiremockServer,GoRulesWiremockServer, etc.) and keep rawRequest.Create()calls inside these helpers. - Keep helper class constructors uniform:
public sealed class XyzWiremockServer(WireMockServerWrapper serverWrapper)withprivate readonly WireMockServer _wireMockServer = serverWrapper.Server;. - In API component suites, couple one wrapper instance to one fixture instance so fixtures can run in parallel without shared stub state.
Template: Wrapper
public sealed class WireMockServerWrapper : IDisposable
{
private WireMockServer _wireMockServer = null!;
public WireMockServer Server => _wireMockServer;
public HttpClient HttpClient => _wireMockServer.CreateClient();
public string Url => _wireMockServer.Url!;
public void Start()
{
Start(new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Include
});
}
public void Start(JsonSerializerSettings jsonSerializerSettings)
{
_wireMockServer = WireMockServer.Start(new WireMockServerSettings
{
StartAdminInterface = true,
JsonSerializerSettings = jsonSerializerSettings
});
}
public void Reset() => _wireMockServer.Reset();
public void Dispose() => _wireMockServer.Dispose();
public void Stop() => Dispose();
}Template: Dependency Helper
public sealed class CustomerTariffApiWiremockServer(WireMockServerWrapper serverWrapper, Guid userId)
{
private readonly WireMockServer _wireMockServer = serverWrapper.Server;
public void GivenCustomerTariffs(Guid customerId, CustomerTariffsResponse response)
{
_wireMockServer
.Given(Request.Create()
.UsingMethod("GET")
.WithPath("/customer/tariffs")
.WithHeader("X-On-Behalf-Of-User", userId.ToString())
.WithParam("customerId", customerId.ToString()))
.RespondWith(Response.Create()
.WithStatusCode(200)
.WithHeader("Content-Type", "application/json")
.WithBodyAsJson(_ => response));
}
}Stub Rules
- Keep stub definitions close to scenario intent.
- Use named helpers for common upstream responses.
- Reset mappings and request logs between tests.
Verification
- Assert expected outbound calls (count + payload semantics).
- Assert no unexpected calls for negative scenarios.
- Keep provider-specific stubs grouped in fixture partial files (
Fixture.Card2Card.cs,Fixture.Crypto.cs, etc.).
Failure Simulation
- Model timeout, 4xx, 5xx, malformed payload, and slow responses.
- Verify API/component mapping behavior for each failure class.