Write integration tests
Skill jzills/claude-marketplace/plugins/dotnet-integration-tests/skills/write-integration-tests
A Claude Code plugin marketplace with skills for git workflows, code quality, safety, and more.
npx -y skills add jzills/claude-marketplace --skill write-integration-testsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill whenever the user asks to write, generate, or add integration tests for C# or .NET code. Trigger on: "write integration tests", "add integration tests", "test this endpoint", "test this API", "integration test for this controller", "test against the database", "write end-to-end tests", "test this repository with a real database", "add integration test coverage". Also trigger when the user shows a controller, repository, or service and asks for tests that require real infrastructure (database, message broker, HTTP, etc.) rather than mocks.
SKILL.md
10.6 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
.NET Integration Test Writer
You are writing production-quality integration tests for C# code using NUnit, Testcontainers (or docker-compose), and FluentAssertions. Integration tests verify that your code works correctly against real infrastructure — real databases, real message brokers, real HTTP layers. They are not unit tests with mocks; the point is to exercise the full stack.
Pre-flight Check
Before writing any tests, do the following:
-
Read the class or file under test. Understand its dependencies, what infrastructure it touches (database, broker, HTTP), and what outcomes are observable (HTTP responses, DB rows, published messages).
-
Look for an existing integration test project. Search for
.csprojfiles that referenceMicrosoft.AspNetCore.Mvc.Testingor anyTestcontainers.*package. Check sibling directories of the source project (e.g.MyProject.IntegrationTests,MyProject.Tests.Integration). -
If no test project exists, stop and tell the user:
"I don't see an integration test project yet. Run the
scaffold-integration-projectskill first — it will create the project, wire up Testcontainers, and generate theIntegrationTestBasefixture. Then come back and I'll write the tests." -
If a test project exists, read its
.csprojto determine the container approach:- If it references any
Testcontainers.*package → Testcontainers approach (see Step 4A below). - If it does not → docker-compose approach (see Step 4B below).
- If it references any
-
Look for an
IntegrationTestBaseclass (generated by thesetup-test-infrastructureskill). If it exists, inherit from it rather than re-declaring lifecycle code.
Step 4 — Infrastructure Sub-skill
If the test project exists but has no container fixture or IntegrationTestBase, invoke the infrastructure sub-skill before writing tests:
REQUIRED SUB-SKILL: Invoke setup-test-infrastructure with args:
"source csproj: <path-to-source.csproj>, test project dir: <path-to-test-project/>"
The generated fixture files will be available for the sub-steps below.
Test Structure: AAA
Every test follows Arrange / Act / Assert. Separate the three sections with blank lines.
Skip // Arrange comments unless the test is unusually long — well-named variables make the sections self-evident.
[Test]
public async Task GetProduct_WhenProductExists_Returns200WithBody()
{
var productId = await SeedProductAsync(name: "Widget", price: 9.99m);
var response = await Client.GetAsync($"/api/products/{productId}");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var body = await response.Content.ReadFromJsonAsync<ProductDto>();
body.Should().NotBeNull();
body!.Name.Should().Be("Widget");
body.Price.Should().Be(9.99m);
}
Naming Convention
Use the format: MethodName_Condition_ExpectedOutcome
CreateOrder_WhenPayloadIsValid_Returns201WithLocationGetUser_WhenUserDoesNotExist_Returns404PlaceOrder_WhenStockIsEmpty_PublishesOutOfStockEventDeleteProduct_WhenUserIsUnauthorized_Returns401GetAll_Always_ReturnsOnlyRowsBelongingToTenant(useAlwayswhen there is no meaningful condition)
Failures should be self-documenting — the test name alone should tell you exactly what broke.
Fixture Setup
Use [OneTimeSetUp] for expensive shared resources (containers, WebApplicationFactory, migrations).
Use [SetUp] for per-test state: seeding rows, resetting queues, clearing caches.
Use [TearDown] to remove test-seeded data so tests remain independent.
[TestFixture]
public class ProductsEndpointTests : IntegrationTestBase
{
private Guid _seededProductId;
[SetUp]
public async Task SetUp()
{
_seededProductId = await SeedProductAsync(name: "Widget", price: 9.99m);
}
[TearDown]
public async Task TearDown()
{
await CleanUpProductsAsync();
}
}
When inheriting from IntegrationTestBase, do not redeclare [OneTimeSetUp] / [OneTimeTearDown] for container lifecycle — the base class owns that. Per-fixture [SetUp] and [TearDown] in derived classes are fine.
Step 4A — Testcontainers Approach
Inherit from IntegrationTestBase (generated by setup-test-infrastructure).
IntegrationTestBase owns the container lifecycle and exposes Client (an HttpClient) and Infrastructure (the container fixture).
For EF Core tests that need direct DB access, resolve a scoped DbContext from the factory's service provider:
using var scope = Factory.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
For each scenario, refer to the matching pattern in references/integration-test-patterns.md:
- ASP.NET Core controller/endpoint tests → Pattern 1: WebApplicationFactory pattern
- EF Core repository tests → Pattern 2: EF Core repository pattern
- Any test needing data setup and cleanup → Pattern 3: Data seeding and cleanup
- Message consumer/handler tests → Pattern 4: Message consumer pattern
Step 4B — Docker Compose Approach
When the test project does not use Testcontainers, note at the top of every generated test file:
// Prerequisites: run `docker compose up -d` from the test project root before executing these tests.
// Connection strings are read from environment variables or appsettings.Testing.json.
Read connection strings from IConfiguration / environment variables — never hardcode them.
var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__Postgres")
?? configuration.GetConnectionString("Postgres")
?? throw new InvalidOperationException("Postgres connection string not configured.");
Test Patterns by Scenario
For each scenario, refer to the concrete C# examples in references/integration-test-patterns.md.
| Scenario | Pattern to use |
|---|---|
| ASP.NET Core controller / endpoint | Pattern 1 — WebApplicationFactory |
| EF Core repository with real DB | Pattern 2 — EF Core repository |
| Data setup and cleanup strategies | Pattern 3 — Seeding and cleanup |
| Message consumer / handler | Pattern 4 — Message consumer |
What to Test
For every class or endpoint under test, identify and cover:
- Happy paths — valid inputs produce the expected response, DB state, or side effect
- Not-found cases — missing resources return 404 (HTTP) or
null/ empty (repositories) - Validation failures — invalid payloads return 400 with a useful problem detail
- Authorization failures — unauthenticated requests return 401; unauthorized (wrong role/tenant) return 403
- Boundary conditions — empty collections, zero values, maximum lengths
For HTTP tests, always assert both the status code and the response body shape.
Assertions with FluentAssertions
FluentAssertions produces failure messages that tell you exactly what went wrong.
// HTTP status
response.StatusCode.Should().Be(HttpStatusCode.Created);
// Response body
var body = await response.Content.ReadFromJsonAsync<OrderDto>();
body.Should().NotBeNull();
body!.Id.Should().NotBeEmpty();
body.Total.Should().Be(99.99m);
// Collections
var items = await response.Content.ReadFromJsonAsync<List<ProductDto>>();
items.Should().HaveCount(3);
items.Should().Contain(product => product.IsActive);
// Database state
var row = await db.Orders.FindAsync(orderId);
row.Should().NotBeNull();
row!.Status.Should().Be(OrderStatus.Confirmed);
// Exceptions (async)
Func<Task> act = () => repository.GetByIdAsync(-1);
await act.Should().ThrowAsync<ArgumentOutOfRangeException>();
Code Style
Lambda parameters: Use the type name (singular) rather than x. This makes intent clear without needing to look up what x refers to.
// Preferred
items.Should().Contain(product => product.IsActive);
items.Should().AllSatisfy(order => order.TenantId.Should().Be(expectedTenantId));
// Avoid
items.Should().Contain(x => x.IsActive);
Async Tests
Always await async methods. NUnit supports async Task test methods natively.
Never use .Result or .Wait() — they deadlock in async contexts and hide exceptions.
[Test]
public async Task CreateProduct_WhenPayloadIsValid_Returns201()
{
var payload = new CreateProductRequest { Name = "Gadget", Price = 49.99m };
var response = await Client.PostAsJsonAsync("/api/products", payload);
response.StatusCode.Should().Be(HttpStatusCode.Created);
}
What NOT to Do
- Do not use
Thread.Sleep— useawait-based polling or deterministic Testcontainers health checks (seeWaitUntilAsyncin Pattern 4) - Do not share mutable state between tests — use
[SetUp]for per-test seeds and[TearDown]for cleanup - Do not assert on implementation details — assert on HTTP responses, DB state, and published messages
- Do not use
InMemoryDatabasefor integration tests — the entire point is to test against real infrastructure - Do not mock infrastructure in integration tests — mock only external third-party HTTP calls using WireMock.NET or similar
- Do not use
.Resultor.Wait()on async operations - Do not leave test data in the database after each test — always clean up in
[TearDown]
Output Format
When generating tests for a class or endpoint:
- Read the class under test — understand every public method or route, its inputs, return types, and infrastructure dependencies
- Identify test cases — happy paths, not-found cases, validation failures, authorization failures, boundary conditions
- Write a complete, compilable test file — include all
usingstatements, the correct namespace, and[TestFixture]class scaffold - Name the test class
{ClassName}Testsand place it in the same namespace as the class under test, plus.IntegrationTests - One test class per controller / repository / service under test
Always output the full file, not isolated snippets, unless the user explicitly asks for just one test.