
scaffold-test
by aalmada
Full-stack .NET online book store application with event-sourced backend API and Blazor frontend, orchestrated by Aspire.
SKILL.md
name: scaffold-test description: Create integration tests for API endpoints with SSE event verification and TUnit patterns. Use this when you need to test a new endpoint.
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:
using TUnit.Core; using TUnit.Assertions.Extensions; using BookStore.Shared.Models; namespace BookStore.AppHost.Tests.Tests; public class AuthorCrudTests { // Test methods here }
- Create file in
-
Write Create Test (with SSE)
- Test endpoint that creates a resource
- Use TestHelpers for SSE event verification
- Example:
[Test] public async Task CreateAuthor_ValidRequest_CreatesAndNotifies() { // Arrange var client = await TestHelpers.GetAuthenticatedClientAsync(); var request = TestHelpers.GenerateFakeAuthorRequest(); // Act - ExecuteAndWaitForEventAsync automatically: // 1. Makes the HTTP request // 2. Waits for SSE notification // 3. Returns the created resource var (author, _) = await TestHelpers.ExecuteAndWaitForEventAsync<AuthorDto>( client, async () => await client.PostAsJsonAsync("/api/admin/authors", request), "AuthorUpdated", // Wait for this SSE event timeout: TimeSpan.FromSeconds(10) ); // Assert await Assert.That(author).IsNotNull(); await Assert.That(author!.Name).IsEqualTo(request.Name); await Assert.That(author.Biography).IsEqualTo(request.Biography); }
-
Write Update Test
- Test endpoint that updates a resource
- Pattern: Create → Update → Verify
[Test] public async Task UpdateAuthor_ValidRequest_UpdatesAndNotifies() { // Arrange var client = await TestHelpers.GetAuthenticatedClientAsync(); var createRequest = TestHelpers.GenerateFakeAuthorRequest(); // Create author first var (created, _) = await TestHelpers.ExecuteAndWaitForEventAsync<AuthorDto>( client, async () => await client.PostAsJsonAsync("/api/admin/authors", createRequest), "AuthorUpdated" ); var updateRequest = new UpdateAuthorRequest( Name: "Updated Name", Biography: "Updated Biography" ); // Act var (updated, _) = await TestHelpers.ExecuteAndWaitForEventAsync<AuthorDto>( client, async () => await client.PutAsJsonAsync($"/api/admin/authors/{created!.Id}", updateRequest), "AuthorUpdated" ); // Assert await Assert.That(updated).IsNotNull(); await Assert.That(updated!.Name).IsEqualTo("Updated Name"); await Assert.That(updated.Biography).IsEqualTo("Updated Biography"); await Assert.That(updated.Id).IsEqualTo(created.Id); // Same ID }
-
Write Delete Test (Soft Delete)
- Test soft deletion with restore capability
[Test] public async Task DeleteAuthor_ExistingAuthor_SoftDeletes() { // Arrange var client = await TestHelpers.GetAuthenticatedClientAsync(); var request = TestHelpers.GenerateFakeAuthorRequest(); var (created, _) = await TestHelpers.ExecuteAndWaitForEventAsync<AuthorDto>( client, async () => await client.PostAsJsonAsync("/api/admin/authors", request), "AuthorUpdated" ); // Act - Delete var deleteResponse = await client.DeleteAsync($"/api/admin/authors/{created!.Id}"); await Assert.That(deleteResponse.IsSuccessStatusCode).IsTrue(); // Verify not in public list var listResponse = await TestHelpers.GetUnauthenticatedClient() .GetFromJsonAsync<PagedListDto<AuthorDto>>("/api/authors"); await Assert.That(listResponse).IsNotNull(); await Assert.That(listResponse!.Items.Any(a => a.Id == created.Id)).IsFalse(); }
- Test soft deletion with restore capability
-
Write Query Tests
- Test GET endpoints without SSE
[Test] public async Task GetAuthors_ReturnsPagedList() { // Arrange var client = TestHelpers.GetUnauthenticatedClient(); // Act var response = await client.GetFromJsonAsync<PagedListDto<AuthorDto>>( "/api/authors?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 client = await TestHelpers.GetAuthenticatedClientAsync(); var request = TestHelpers.GenerateFakeAuthorRequest(); var (created, _) = await TestHelpers.ExecuteAndWaitForEventAsync<AuthorDto>( client, async () => await client.PostAsJsonAsync("/api/admin/authors", request), "AuthorUpdated" ); // Act - Get by ID (public endpoint) var unauthClient = TestHelpers.GetUnauthenticatedClient(); var author = await unauthClient.GetFromJsonAsync<AuthorDto>( $"/api/authors/{created!.Id}" ); // Assert await Assert.That(author).IsNotNull(); await Assert.That(author!.Id).IsEqualTo(created.Id); await Assert.That(author.Name).IsEqualTo(request.Name); }
- Test GET endpoints without SSE
-
Add Custom Test Helper (if needed)
- For resource-specific operations, add to
TestHelpers.cs:public static async Task<AuthorDto> CreateAuthorAsync( HttpClient client, CreateAuthorRequest? request = null) { request ??= GenerateFakeAuthorRequest(); var (author, _) = await ExecuteAndWaitForEventAsync<AuthorDto>( client, async () => await client.PostAsJsonAsync("/api/admin/authors", request), "AuthorUpdated" ); return author!; } public static CreateAuthorRequest GenerateFakeAuthorRequest() { var faker = new Faker(); return new CreateAuthorRequest( Name: faker.Name.FullName(), Biography: faker.Lorem.Paragraph() ); }
- For resource-specific operations, add to
-
Test Error Cases
- Test validation failures and edge cases
[Test] public async Task CreateAuthor_EmptyName_ReturnsBadRequest() { // Arrange var client = await TestHelpers.GetAuthenticatedClientAsync(); var request = new CreateAuthorRequest(Name: "", Biography: "Bio"); // Act var response = await client.PostAsJsonAsync("/api/admin/authors", request); // Assert await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.BadRequest); } [Test] public async Task DeleteAuthor_Unauthenticated_ReturnsUnauthorized() { // Arrange var client = TestHelpers.GetUnauthenticatedClient(); var authorId = Guid.CreateVersion7(); // Act var response = await client.DeleteAsync($"/api/admin/authors/{authorId}"); // Assert await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Unauthorized); }
- Test validation failures and edge cases
Key Testing Patterns
Use Authenticated Client for Admin Endpoints
var client = await TestHelpers.GetAuthenticatedClientAsync();
Use Unauthenticated Client for Public Endpoints
var client = TestHelpers.GetUnauthenticatedClient();
Multi-Tenancy
// Manual tenant isolation testing
var client = await TestHelpers.GetAuthenticatedClientAsync();
client.DefaultRequestHeaders.Add("X-Tenant-ID", "acme");
Wait for SSE Events After Mutations
var (result, notification) = await TestHelpers.ExecuteAndWaitForEventAsync<T>(
client,
async () => /* HTTP call */,
"EventName"
);
Use Bogus for Fake Data
var faker = new Faker();
var name = faker.Name.FullName();
var email = faker.Internet.Email();
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);
// Exceptions
await Assert.That(() => action()).Throws<InvalidOperationException>();
Running Tests
Once tests are created, use the dedicated test runner skills:
/run-integration-tests- Execute all integration tests with Aspire/run-unit-tests- Execute unit tests for API and analyzers/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
/run-integration-tests
# Specific test class
dotnet test --filter "FullyQualifiedName~AuthorCrudTests"
# Complete verification
/verify-feature
Troubleshooting
Test Hangs on SSE Wait
- Check event name matches exactly (case-sensitive)
- Verify
MartenCommitListenersends the notification - Increase timeout if needed
Port Already in Use
- Stop any running Aspire instances
- Check for orphaned
dotnetprocesses
"Zero tests ran"
- Ensure test class is public
- Ensure methods are decorated with
[Test] - Check GlobalSetup completed successfully
Related Skills
Prerequisites:
- Feature must be implemented first - see scaffolding skills:
/scaffold-write- Backend mutations/scaffold-read- Backend queries/scaffold-frontend-feature- UI components
Next Steps:
/run-integration-tests- Execute the tests you created/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
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
1ヶ月以内に更新
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon

