Agent skill
test__integration_scaffold
Create integration tests for API endpoints with SSE event verification and TUnit patterns. Use this when you need to test a new endpoint.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/test-integration-scaffold
SKILL.md
Follow this guide to create integration tests for API endpoints in tests/BookStore.AppHost.Tests.
-
Create Test Class
- Create file in
tests/BookStore.AppHost.Tests/ - Naming:
{Feature}Tests.cs(e.g.,AuthorCrudTests.cs) - Template:
csharp
using BookStore.AppHost.Tests.Helpers; using BookStore.Client; using BookStore.Shared.Models; using Refit; using System.Net; namespace BookStore.AppHost.Tests; public class AuthorCrudTests { // Test methods here }
- Create file in
-
Write Create Test (with SSE)
- Test endpoint that creates a resource
- Use Resource Helpers for SSE event verification and creation
- Example:
csharp
[Test] public async Task CreateAuthor_EndToEndFlow_ShouldReturnOk() { // Arrange var client = await HttpClientHelpers.GetAuthenticatedClientAsync<IAuthorsClient>(); var createRequest = FakeDataGenerators.GenerateFakeAuthorRequest(); // Act - Helper handles: // 1. Making the HTTP request // 2. Waiting for SSE notification // 3. Fetching and returning the created resource var author = await AuthorHelpers.CreateAuthorAsync(client, createRequest); // Assert await Assert.That(author).IsNotNull(); await Assert.That(author!.Id).IsNotEqualTo(Guid.Empty); }
-
Write Update Test
- Test endpoint that updates a resource
- Pattern: Create → Update → Verify
csharp
[Test] public async Task UpdateAuthor_ShouldReturnOk() { // Arrange var client = await HttpClientHelpers.GetAuthenticatedClientAsync<IAuthorsClient>(); var createRequest = FakeDataGenerators.GenerateFakeAuthorRequest(); // Create author first var author = await AuthorHelpers.CreateAuthorAsync(client, createRequest); var updateRequest = FakeDataGenerators.GenerateFakeUpdateAuthorRequest(); // Act - Helper handles ETag retrieval and SSE wait author = await AuthorHelpers.UpdateAuthorAsync(client, author!, updateRequest); // Assert - Verify update by fetching var updatedAuthor = await client.GetAuthorAsync(author!.Id); await Assert.That(updatedAuthor.Name).IsEqualTo(updateRequest.Name); }
-
Write Delete Test (Soft Delete)
- Test soft deletion with restore capability
csharp
[Test] public async Task DeleteAuthor_ShouldReturnNoContent() { // Arrange var client = await HttpClientHelpers.GetAuthenticatedClientAsync<IAuthorsClient>(); var createRequest = FakeDataGenerators.GenerateFakeAuthorRequest(); var author = await AuthorHelpers.CreateAuthorAsync(client, createRequest); // Act - Delete author = await AuthorHelpers.DeleteAuthorAsync(client, author!); // Verify - Should return 404 from public API var publicClient = HttpClientHelpers.GetUnauthenticatedClient<IAuthorsClient>(); try { await publicClient.GetAuthorAsync(author!.Id); Assert.Fail("Author should have been deleted"); } catch (ApiException ex) { await Assert.That(ex.StatusCode).IsEqualTo(HttpStatusCode.NotFound); } }
- Test soft deletion with restore capability
-
Write Query Tests
- Test GET endpoints without SSE
csharp
[Test] public async Task GetAuthors_ReturnsPagedList() { // Arrange var client = HttpClientHelpers.GetUnauthenticatedClient<IAuthorsClient>(); // Act var response = await client.GetAuthorsAsync(page: 1, pageSize: 20); // Assert await Assert.That(response).IsNotNull(); await Assert.That(response!.Items).IsNotNull(); await Assert.That(response.TotalCount).IsGreaterThanOrEqualTo(0); } [Test] public async Task GetAuthorById_ExistingId_ReturnsAuthor() { // Arrange - Create an author first var adminClient = await HttpClientHelpers.GetAuthenticatedClientAsync<IAuthorsClient>(); var createRequest = FakeDataGenerators.GenerateFakeAuthorRequest(); var created = await AuthorHelpers.CreateAuthorAsync(adminClient, createRequest); // Act - Get by ID (public endpoint) var publicClient = HttpClientHelpers.GetUnauthenticatedClient<IAuthorsClient>(); var author = await publicClient.GetAuthorAsync(created!.Id); // Assert await Assert.That(author).IsNotNull(); await Assert.That(author!.Id).IsEqualTo(created.Id); await Assert.That(author.Name).IsEqualTo(createRequest.Name); }
- Test GET endpoints without SSE
-
Add Custom Test Helper (if needed)
- For resource-specific operations, create separate helper files:
- AuthorHelpers.cs (example):
csharp
using BookStore.Client; using BookStore.Shared.Models; namespace BookStore.AppHost.Tests.Helpers; public static class AuthorHelpers { public static async Task<AuthorDto> CreateAuthorAsync( IAuthorsClient client, CreateAuthorRequest createRequest) { var received = await SseEventHelpers.ExecuteAndWaitForEventAsync( createRequest.Id, ["AuthorCreated", "AuthorUpdated"], async () => { var response = await client.CreateAuthorWithResponseAsync(createRequest); if (response.Error != null) throw response.Error; }, TestConstants.DefaultEventTimeout); if (!received) throw new Exception("Failed to receive AuthorCreated event."); return await client.GetAuthorAsync(createRequest.Id); } public static async Task<AuthorDto> UpdateAuthorAsync( IAuthorsClient client, AuthorDto author, UpdateAuthorRequest updateRequest) { var version = ETagHelper.ParseETag(author.ETag) ?? 0; var received = await SseEventHelpers.ExecuteAndWaitForEventWithVersionAsync( author.Id, "AuthorUpdated", async () => await client.UpdateAuthorAsync(author.Id, updateRequest, author.ETag), TestConstants.DefaultEventTimeout, minVersion: version + 1, minTimestamp: DateTimeOffset.UtcNow); if (!received.Success) throw new Exception("Failed to receive AuthorUpdated event."); return await client.GetAuthorAsync(author.Id); } public static async Task<AuthorDto> DeleteAuthorAsync( IAuthorsClient client, AuthorDto author) { var received = await SseEventHelpers.ExecuteAndWaitForEventAsync( author.Id, "AuthorDeleted", async () => { var etag = author.ETag; if (string.IsNullOrEmpty(etag)) { var latest = await client.GetAuthorAdminAsync(author.Id); etag = latest?.ETag; } await client.SoftDeleteAuthorAsync(author.Id, etag); }, TestConstants.DefaultEventTimeout); if (!received) throw new Exception("Failed to receive AuthorDeleted event."); return await client.GetAuthorAsync(author.Id); } } - FakeDataGenerators.cs (add fake data generators):
csharp
using Bogus; using BookStore.Client; using BookStore.Shared.Models; namespace BookStore.AppHost.Tests.Helpers; public static class FakeDataGenerators { static readonly Faker _faker = new(); public static CreateAuthorRequest GenerateFakeAuthorRequest() => new() { Id = Guid.CreateVersion7(), Name = _faker.Name.FullName(), Translations = new Dictionary<string, AuthorTranslationDto> { ["en"] = new(_faker.Lorem.Paragraphs(2)), ["es"] = new(_faker.Lorem.Paragraphs(2)) } }; public static UpdateAuthorRequest GenerateFakeUpdateAuthorRequest() => new() { Name = _faker.Name.FullName(), Translations = new Dictionary<string, AuthorTranslationDto> { ["en"] = new(_faker.Lorem.Paragraphs(2)), ["es"] = new(_faker.Lorem.Paragraphs(2)) } }; }
-
Test Error Cases
- Test validation failures and edge cases
csharp
[Test] [Arguments("")] [Arguments(null)] public async Task CreateAuthor_WithInvalidName_ShouldReturnBadRequest(string? invalidName) { // Arrange var client = await HttpClientHelpers.GetAuthenticatedClientAsync<IAuthorsClient>(); var request = new CreateAuthorRequest { Id = Guid.CreateVersion7(), Name = invalidName, Translations = new Dictionary<string, AuthorTranslationDto> { ["en"] = new("Biography") } }; // Act & Assert - Refit throws ApiException on error try { await client.CreateAuthorAsync(request); Assert.Fail("Expected ApiException was not thrown"); } catch (ApiException ex) { await Assert.That(ex.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } } [Test] public async Task DeleteAuthor_Unauthenticated_ReturnsUnauthorized() { // Arrange var client = HttpClientHelpers.GetUnauthenticatedClient<IAuthorsClient>(); var authorId = Guid.CreateVersion7(); // Act & Assert try { await client.SoftDeleteAuthorAsync(authorId); Assert.Fail("Expected ApiException was not thrown"); } catch (ApiException ex) { await Assert.That(ex.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized); } } // Validation with error codes (ProblemDetails pattern) [Test] [Arguments("", ErrorCodes.Books.TitleRequired)] [Arguments(null, ErrorCodes.Books.TitleRequired)] public async Task CreateBook_WithInvalidTitle_ReturnsExpectedErrorCode( string? title, string expectedErrorCode) { // Arrange var client = await HttpClientHelpers.GetAuthenticatedClientAsync<IBooksClient>(); var request = FakeDataGenerators.GenerateFakeBookRequest(); request.Title = title; // Act & Assert try { await client.CreateBookAsync(request); Assert.Fail("Expected ApiException was not thrown"); } catch (ApiException ex) { await Assert.That(ex.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); await Assert.That(ex.Content).Contains(expectedErrorCode); } }
- Test validation failures and edge cases
Key Testing Patterns
Use Authenticated Refit Client for Admin Endpoints
var client = await HttpClientHelpers.GetAuthenticatedClientAsync<IAuthorsClient>();
Use Unauthenticated Refit Client for Public Endpoints
var client = HttpClientHelpers.GetUnauthenticatedClient<IAuthorsClient>();
Multi-Tenancy Testing
// Use specific tenant
var httpClient = HttpClientHelpers.GetUnauthenticatedClient("tenant-id");
var client = RestService.For<IAuthorsClient>(httpClient);
// Or for authenticated requests
var httpClient = await HttpClientHelpers.GetTenantClientAsync("tenant-id", accessToken);
var client = RestService.For<IAuthorsClient>(httpClient);
Wait for SSE Events After Mutations
// Simple event wait
var received = await SseEventHelpers.ExecuteAndWaitForEventAsync(
entityId,
"EventName",
async () => /* HTTP call */,
TestConstants.DefaultEventTimeout
);
// Wait for multiple event types
var received = await SseEventHelpers.ExecuteAndWaitForEventAsync(
entityId,
["EventName1", "EventName2"],
async () => /* HTTP call */,
TestConstants.DefaultEventTimeout
);
// Wait with version check (for updates)
var result = await SseEventHelpers.ExecuteAndWaitForEventWithVersionAsync(
entityId,
"EventName",
async () => /* HTTP call */,
TestConstants.DefaultEventTimeout,
minVersion: currentVersion + 1,
minTimestamp: DateTimeOffset.UtcNow
);
Use FakeDataGenerators for Test Data
var createRequest = FakeDataGenerators.GenerateFakeAuthorRequest();
var updateRequest = FakeDataGenerators.GenerateFakeUpdateAuthorRequest();
var email = FakeDataGenerators.GenerateFakeEmail();
var password = FakeDataGenerators.GenerateFakePassword();
Use TestConstants for Timeouts
TestConstants.DefaultTimeout // 30 seconds
TestConstants.DefaultEventTimeout // 30 seconds
TestConstants.DefaultProjectionDelay // 500 ms
TestConstants.DefaultRetryDelay // 100 ms
TestConstants.DefaultPollingInterval // 50 ms
TestConstants.DefaultStreamTimeout // 5 minutes
TestConstants.DefaultMaxRetries // 10
Handle ETags for Concurrency
// Get resource with ETag
var response = await client.GetAuthorWithResponseAsync(authorId);
var etag = response.Headers.ETag?.Tag;
// Use ETag in update/delete
await client.UpdateAuthorAsync(authorId, updateRequest, etag);
await client.SoftDeleteAuthorAsync(authorId, etag);
// Parse ETag for version comparison
var version = ETagHelper.ParseETag(author.ETag) ?? 0;
TUnit Assertion Patterns
// Equality
await Assert.That(actual).IsEqualTo(expected);
// Null checks
await Assert.That(value).IsNotNull();
await Assert.That(value).IsNull();
// Boolean
await Assert.That(condition).IsTrue();
await Assert.That(condition).IsFalse();
// Collections
await Assert.That(collection).Contains(item);
await Assert.That(collection).DoesNotContain(item);
// Numeric comparisons
await Assert.That(count).IsGreaterThan(0);
await Assert.That(count).IsGreaterThanOrEqualTo(0);
// Refit Exception handling (use try/catch with Assert.Fail)
try
{
await client.SomeMethodAsync();
Assert.Fail("Expected ApiException was not thrown");
}
catch (ApiException ex)
{
await Assert.That(ex.StatusCode).IsEqualTo(HttpStatusCode.BadRequest);
}
Running Tests
Once tests are created, use the dedicated test runner skills:
/test__integration_suite- Execute all integration tests with Aspire/test__unit_suite- Execute unit tests for API and analyzers/test__verify_feature- Complete verification (build + format + all tests)
For specific test filtering or manual commands, see:
- run-integration-tests - Integration test details
- run-unit-tests - Unit test details
Quick Reference
# All integration tests
/test__integration_suite
# Specific test class
dotnet test --filter "FullyQualifiedName~AuthorCrudTests"
# Complete verification
/test__verify_feature
Troubleshooting
Test Hangs on SSE Wait
- Check event name matches exactly (case-sensitive)
- Verify
MartenCommitListenersends the notification - Increase timeout if needed using
TestConstants - Check that the entity ID matches (use
Guid.Emptyto match any entity)
Port Already in Use
- Stop any running Aspire instances
- Check for orphaned
dotnetprocesses - Use
pkill -f dotneton macOS/Linux to clean up
"Zero tests ran"
- Ensure test class is public
- Ensure methods are decorated with
[Test] - Check
GlobalHookssetup completed successfully - Verify test assembly references TUnit
Refit Exceptions Not Caught
- Use try/catch blocks with
ApiExceptionfor error testing - Call
Assert.Fail()if no exception was thrown - Check status code on caught exception
ETag Missing or Null
- Use
*WithResponseAsync()variants to access response headers - Access ETag via
response.Headers.ETag?.Tag - Use
ETagHelper.ParseETag()to get version number
Related Skills
Prerequisites:
- Feature must be implemented first - see scaffolding skills:
/wolverine__create_operation- Backend create mutations/wolverine__update_operation- Backend update mutations/marten__get_by_id- Backend single-resource queries/marten__list_query- Backend list queries/frontend__feature_scaffold- UI components
Next Steps:
/test__integration_suite- Execute the tests you created/test__verify_feature- Complete verification workflow- Check coverage and add edge cases for boundary conditions
See Also:
- verify-feature - Definition of Done verification
- run-integration-tests - Integration test execution
- run-unit-tests - Unit test execution
- integration-testing-guide - Aspire integration testing
- testing-guide - TUnit unit testing
- AppHost.Tests AGENTS.md - Test project patterns
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?