Generate comprehensive ServerRequest tests using VaporTesting infrastructure. Use when testing any ServerRequest type (ShowRequest, ViewModelRequest, CreateRequest, UpdateRequest, DeleteRequest) against a Vapor server.
Generates comprehensive ServerRequest tests using VaporTesting infrastructure. Use when testing any ServerRequest type (ShowRequest, ViewModelRequest, CreateRequest, UpdateRequest, DeleteRequest) against a Vapor server.
/plugin marketplace add foscomputerservices/FOSUtilities/plugin install fosmvvm-generators@fosmvvm-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Generate test files for ServerRequest types using VaporTesting infrastructure.
For full architecture context, see FOSMVVMArchitecture.md
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! │
│ │
└──────────────────────────────────────────────────────────────────────┘
// ❌ 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)
// ✅ 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) |
func 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 - Spanishimport 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 |
Determine which ServerRequest types need coverage:
Generate test file conforming to testing patterns:
@Test function per scenario (success, not found, validation error, etc.)app.testing().test(request, locale:) for ALL request testingSet up Vapor Application with required:
swift test --filter {TestSuiteName}
@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")
}
}
}
@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)
}
}
}
@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")
}
}
}
@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)
}
}
}
@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)
}
}
}
Cause: Controller not registered in test app.
Fix: Register the controller before testing:
try 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:
try 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 |
swift test --filter {TestSuiteName}See reference.md for complete file templates.
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2025-01-05 | Initial skill |
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.