Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Generate ServerRequest tests using VaporTesting. Covers typed request/response validation for Show, Create, Update, and Delete operations.
.claude/skills/fosmvvm-serverrequest-test-generator/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 8 |
| gemini-3.1-pro-preview | 100% | 1 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-22 | ✗→✓ | ▲ Improved | — | — |
Generate test files for ServerRequest types using VaporTesting infrastructure.
> For full architecture context, see FOSMVVMArchitecture.md | OpenClaw reference
ServerRequest testing uses VaporTesting infrastructure to send typed requests through the full server stack:
┌─────────────────────────────────────────────────────────────────────┐
│ ServerRequest Test Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Test Code: │
│ let request = MyRequest(query: .init(...)) │
│ app.testing().test(request, locale: en) { response in } │
│ │
│ Infrastructure handles: │
│ • Path derivation from type name (MyRequest → /my) │
│ • HTTP method from action (ShowRequest → GET) │
│ • Query/body encoding │
│ • Header injection (locale, version) │
│ • Response decoding to ResponseBody type │
│ │
│ You verify: │
│ • response.status (HTTPStatus) │
│ • response.body (R.ResponseBody? - typed!) │
│ • response.error (R.ResponseError? - typed!) │
│ │
└─────────────────────────────────────────────────────────────────────┘Testing ServerRequests uses VaporTesting infrastructure. No manual URL construction. Ever.
┌──────────────────────────────────────────────────────────────────────┐
│ SERVERREQUEST TESTING USES TestingApplicationTester │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Configure Vapor Application with routes │
│ 2. Use app.testing().test(request, locale:) { response in } │
│ 3. Verify response.status, response.body, response.error │
│ │
│ TestingServerRequestResponse<R> provides TYPED access to: │
│ • status: HTTPStatus │
│ • headers: HTTPHeaders │
│ • body: R.ResponseBody? ← Auto-decoded! │
│ • error: R.ResponseError? ← Auto-decoded! │
│ │
└──────────────────────────────────────────────────────────────────────┘swift// ❌ WRONG - manual URL construction let url = URL(string: "http://localhost:8080/my_request?query=value")! let response = try await URLSession.shared.data(from: url) // ❌ WRONG - string path with method try await app.test(.GET, "/my_request") { response in } // ❌ WRONG - manual JSON encoding/decoding let json = try JSONEncoder().encode(requestBody) let decoded = try JSONDecoder().decode(ResponseBody.self, from: data) // ❌ WRONG - constructing TestingHTTPRequest manually let httpRequest = TestingHTTPRequest(method: .GET, url: "/path", headers: headers) try await app.testing().performTest(request: httpRequest)
swift// ✅ RIGHT - Use TestingApplicationTester.test() with ServerRequest let request = MyShowRequest(query: .init(userId: userId)) try await app.testing().test(request, locale: en) { response in #expect(response.status == .ok) #expect(response.body?.viewModel.name == "Expected Name") } // ✅ RIGHT - Test multiple locales for locale in [en, es] { try await app.testing().test(request, locale: locale) { response in #expect(response.status == .ok) // Localized values are automatically handled } } // ✅ RIGHT - Test error responses let badRequest = MyShowRequest(query: .init(userId: invalidId)) try await app.testing().test(badRequest, locale: en) { response in #expect(response.status == .notFound) #expect(response.error != nil) }
The path is derived from the ServerRequest type. HTTP method comes from the action. Headers are automatic. You NEVER write URL strings or decode JSON manually.
If you're about to write URLSession, app.test(.GET, "/path"), or manual JSON decoding, STOP and use this skill instead.
| File | Location | Purpose | |------|----------|---------| | {Feature}RequestTests.swift | Tests/{Target}Tests/Requests/ | Test suite for ServerRequest | | Test YAML (if needed) | Tests/{Target}Tests/TestYAML/ | Localization for test ViewModels |
| Placeholder | Description | Example | |-------------|-------------|---------| | {Feature} | Feature or entity name (PascalCase) | Idea, User, Dashboard | | {Target} | Server test target | WebServerTests, AppTests | | {ViewModelsTarget} | Shared ViewModels SPM target | ViewModels | | {WebServerTarget} | Server-side target | WebServer, AppServer | | {ResourceDir} | YAML resource directory | TestYAML, Resources |
Wraps HTTP response with typed access:
| Property | Type | Description | |----------|------|-------------| | status | HTTPStatus | HTTP status code (.ok, .notFound, etc.) | | headers | HTTPHeaders | Response headers | | body | R.ResponseBody? | Typed response body (auto-decoded) | | error | R.ResponseError? | Typed error (auto-decoded) |
swiftfunc test<R: ServerRequest>( _ request: R, locale: Locale = en, headers: HTTPHeaders = [:], afterResponse: (TestingServerRequestResponse<R>) async throws -> Void ) async throws -> any TestingApplicationTester
Available on TestingApplicationTester:
en - EnglishenUS - English (US)enGB - English (UK)es - Spanishswiftimport FOSFoundation @testable import FOSMVVM import FOSTesting import FOSTestingVapor import Foundation import Testing import Vapor import VaporTesting @Suite("MyFeature Request Tests") struct MyFeatureRequestTests { @Test func showRequest_success() async throws { try await withTestApp { app in let request = MyShowRequest(query: .init(id: validId)) try await app.testing().test(request, locale: en) { response in #expect(response.status == .ok) #expect(response.body?.viewModel != nil) } } } @Test func showRequest_notFound() async throws { try await withTestApp { app in let request = MyShowRequest(query: .init(id: invalidId)) try await app.testing().test(request, locale: en) { response in #expect(response.status == .notFound) } } } } private func withTestApp(_ test: (Application) async throws -> Void) async throws { try await withApp { app in // Configure routes try app.routes.register(collection: MyController()) try await test(app) } }
| Request Type | HTTP Method | What to Test | |--------------|-------------|--------------| | ShowRequest | GET | Query params, response body, localization | | ViewModelRequest | GET | ViewModel population, all localized fields | | CreateRequest | POST | RequestBody validation, created entity, ID response | | UpdateRequest | PATCH | RequestBody validation, updated entity, response | | DeleteRequest | DELETE | Entity removal, status code |
Invocation: /fosmvvm-serverrequest-test-generator
Prerequisites:
Workflow integration: This skill is used when testing ServerRequest implementations. The skill references conversation context automatically—no file paths or Q&A needed. Typically follows fosmvvm-serverrequest-generator.
This skill references conversation context to determine test structure:
From conversation context, the skill identifies:
Based on operation semantics:
From project state:
Skill references information from:
swift@Test func viewModelRequest_multiLocale() async throws { try await withTestApp { app in let request = DashboardViewModelRequest() // Test English try await app.testing().test(request, locale: en) { response in #expect(response.status == .ok) let vm = try #require(response.body) #expect(try vm.pageTitle.localizedString == "Dashboard") } // Test Spanish try await app.testing().test(request, locale: es) { response in #expect(response.status == .ok) let vm = try #require(response.body) #expect(try vm.pageTitle.localizedString == "Tablero") } } }
swift@Test func createRequest_validInput() async throws { try await withTestApp { app in let request = CreateIdeaRequest(requestBody: .init( content: "Valid idea content" )) try await app.testing().test(request, locale: en) { response in #expect(response.status == .ok) #expect(response.body?.id != nil) } } } @Test func createRequest_invalidInput() async throws { try await withTestApp { app in let request = CreateIdeaRequest(requestBody: .init( content: "" // Empty content should fail validation )) try await app.testing().test(request, locale: en) { response in #expect(response.status == .badRequest) #expect(response.error != nil) } } }
swift@Test func updateRequest_success() async throws { try await withTestApp { app in // First create an entity let createRequest = CreateIdeaRequest(requestBody: .init(content: "Original")) var createdId: ModelIdType? try await app.testing().test(createRequest, locale: en) { response in createdId = response.body?.id } // Then update it let updateRequest = UpdateIdeaRequest(requestBody: .init( ideaId: try #require(createdId), content: "Updated content" )) try await app.testing().test(updateRequest, locale: en) { response in #expect(response.status == .ok) #expect(response.body?.viewModel.content == "Updated content") } } }
swift@Test func deleteRequest_success() async throws { try await withTestApp { app in // Create, then delete let deleteRequest = DeleteIdeaRequest(requestBody: .init(ideaId: existingId)) try await app.testing().test(deleteRequest, locale: en) { response in #expect(response.status == .ok) } // Verify deleted (should return not found) let showRequest = ShowIdeaRequest(query: .init(ideaId: existingId)) try await app.testing().test(showRequest, locale: en) { response in #expect(response.status == .notFound) } } }
swift@Test func showRequest_withQuery() async throws { try await withTestApp { app in let request = UserShowRequest(query: .init( userId: userId, includeDetails: true )) try await app.testing().test(request, locale: en) { response in #expect(response.status == .ok) #expect(response.body?.user.details != nil) } } }
Unlike ViewModels, ServerRequestError types:
Stubbable or RetrievablePropertyNamesexpectTranslations(ErrorType.self) like ViewModelsThis means you must manually test each error case individually.
Use LocalizableTestCase.expectTranslations(_ localizable:) on each error's Localizable property:
swift@Suite("MyError Localization Tests") struct MyErrorLocalizationTests: LocalizableTestCase { let locStore: LocalizationStore init() throws { self.locStore = try Self.loadLocalizationStore( bundle: Bundle.module, resourceDirectoryName: "TestYAML" ) } @Test func errorMessages_simpleErrors() throws { // Test each error case individually let serverFailed = MyError(code: .serverFailed) try expectTranslations(serverFailed.message) let appFailed = MyError(code: .applicationFailed) try expectTranslations(appFailed.message) } @Test func errorMessages_withSubstitutions() throws { // For errors with associated values, test with representative values let quotaError = QuotaError(code: .quotaExceeded(requested: 100, maximum: 50)) try expectTranslations(quotaError.message) } }
When testing the full request/response cycle, verify error messages resolve:
swift@Test func createRequest_validationError_hasLocalizedMessage() async throws { try await withTestApp { app in let request = CreateIdeaRequest(requestBody: .init(content: "")) try await app.testing().test(request, locale: en) { response in #expect(response.status == .badRequest) let error = try #require(response.error) // Verify the message resolved (not empty or pending) #expect(!error.message.isEmpty) // Optionally verify specific text for English locale #expect(try error.message.localizedString.contains("required")) } } }
Stubbable works well for ViewModels because:
stub() provides a complete test instanceServerRequestError types are often enums where:
stub() can't cover all casesYou must enumerate and test each error case explicitly.
LocalizableSubstitutionsCause: Controller not registered in test app.
Fix: Register the controller before testing:
swifttry app.routes.register(collection: MyController())
Cause: JSON decoding failed silently.
Fix: Check that ResponseBody type matches server response exactly. Use response.headers to verify Content-Type.
Cause: Locale not passed to encoder.
Fix: The test(_:locale:) method handles this automatically. Ensure you're passing the locale parameter.
Cause: YAML localization not loaded.
Fix: Initialize localization store in test app setup:
swifttry app.initYamlLocalization( bundle: Bundle.module, resourceDirectoryName: "TestYAML" )
| Concept | Convention | Example | |---------|------------|---------| | Test suite | {Feature}RequestTests | IdeaRequestTests | | Test file | {Feature}RequestTests.swift | IdeaRequestTests.swift | | Test method (success) | {action}Request_success | showRequest_success | | Test method (error) | {action}Request_{errorCase} | showRequest_notFound | | Test method (validation) | {action}Request_{validationCase} | createRequest_emptyContent | | Test helper | withTestApp | withTestApp { app in } | | Locale constant | en, es, enUS, enGB | locale: en |
See reference.md for complete file templates.
| Version | Date | Changes | |---------|------|---------| | 1.1 | 2025-01-20 | Add ServerRequestError localization testing guidance | | 1.2 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. | | 1.0 | 2025-01-05 | Initial skill |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +82 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.