Skip to content

HTTP Testing

Stratal provides three classes for HTTP testing: TestHttpClient for creating requests, TestHttpRequest for building request details, and TestResponse for asserting on the response. Together they give you a fluent, chainable API for integration testing your endpoints.

Access the HTTP client through module.http and call a method matching the HTTP verb you need:

// Simple GET
const response = await module.http.get('/api/notes').send()
// POST with body
const response = await module.http
.post('/api/notes')
.withBody({ title: 'My Note', content: 'Hello' })
.send()
MethodReturnsDescription
get(path)TestHttpRequestCreate a GET request
post(path)TestHttpRequestCreate a POST request
put(path)TestHttpRequestCreate a PUT request
patch(path)TestHttpRequestCreate a PATCH request
delete(path)TestHttpRequestCreate a DELETE request
forHost(host)thisSet the Host header for all subsequent requests
withHeaders(headers)thisSet default headers for all subsequent requests

Use forHost() to set the host for multi-tenant applications:

const response = await module.http
.forHost('tenant-a.example.com')
.get('/api/settings')
.send()

Each HTTP method returns a TestHttpRequest that lets you configure the request body, headers, and content type before sending:

const response = await module.http
.post('/api/users')
.withBody({ name: 'Alice', role: 'admin' })
.withHeaders({ 'X-Request-ID': 'test-123' })
.send()
MethodReturnsDescription
withBody(data)thisSet the request body (serialized as JSON)
withHeaders(headers)thisAdd headers to the request
asJson()thisExplicitly set Content-Type: application/json
send()Promise<TestResponse>Send the request and return the response

TestResponse wraps the raw Response object with chainable assertion methods. Status assertions are synchronous, while JSON assertions are async (they parse the body on first access).

MethodAsserts
assertOk()Status is 200
assertCreated()Status is 201
assertNoContent()Status is 204
assertBadRequest()Status is 400
assertUnauthorized()Status is 401
assertForbidden()Status is 403
assertNotFound()Status is 404
assertUnprocessable()Status is 422
assertServerError()Status is 500
assertStatus(code)Status equals the given code
assertSuccessful()Status is in the 2xx range

Status assertions are chainable and return this:

response.assertOk().assertHeader('Content-Type', 'application/json')
MethodAsserts
assertJson(expected)Response JSON contains the given key-value pairs
assertJsonPath(path, expected)Value at dot-notation path equals expected
assertJsonPaths(expectations)Multiple path-value pairs at once
assertJsonStructure(keys)Response JSON has the given top-level keys
assertJsonPathExists(path)Path exists (value can be anything, including null)
assertJsonPathMissing(path)Path does not exist
assertJsonPathMatches(path, matcher)Value at path passes the predicate function
assertJsonPathContains(path, substring)String value at path contains the substring
assertJsonPathIncludes(path, item)Array at path includes the item
assertJsonPathCount(path, count)Array at path has the given length

JSON assertions are async (they return Promise<this>) and must be awaited:

// Single path
await response.assertJsonPath('data.user.name', 'Alice')
// Multiple paths at once
await response.assertJsonPaths({
'data.user.name': 'Alice',
'data.user.role': 'admin',
})
// Verify structure
await response.assertJsonStructure(['data', 'meta'])
// Check existence
await response.assertJsonPathExists('data.id')
await response.assertJsonPathMissing('data.password')
MethodAsserts
assertHeader(name, expected?)Header is present (optionally with the given value)
assertHeaderMissing(name)Header is not present
response
.assertHeader('Content-Type', 'application/json')
.assertHeader('X-Request-ID')
.assertHeaderMissing('X-Debug')

You can also access the underlying response data directly for custom assertions:

Property / MethodTypeDescription
statusnumberResponse status code
headersHeadersResponse headers
rawResponseThe raw Response object
json()Promise<T>Parse body as JSON
text()Promise<string>Get body as text
const data = await response.json<{ data: { id: string } }>()
expect(data.data.id).toMatch(/^[a-f0-9-]+$/)

Here is a complete test suite for a notes API covering create, list, get by ID, update, delete, and 404 handling:

import { Test, type TestingModule } from '@stratal/testing'
import { afterAll, beforeEach, describe, it } from 'vitest'
import { NotesModule } from '../notes.module'
describe('Notes API', () => {
let module: TestingModule
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [NotesModule],
}).compile()
})
afterAll(async () => {
await module.close()
})
it('should create a note', async () => {
const response = await module.http
.post('/api/notes')
.withBody({ title: 'Test Note', content: 'Hello world' })
.send()
response.assertCreated()
await response.assertJsonPath('data.title', 'Test Note')
await response.assertJsonPath('data.content', 'Hello world')
await response.assertJsonPathExists('data.id')
})
it('should list all notes', async () => {
// Create two notes
await module.http
.post('/api/notes')
.withBody({ title: 'Note 1', content: 'First' })
.send()
await module.http
.post('/api/notes')
.withBody({ title: 'Note 2', content: 'Second' })
.send()
const response = await module.http.get('/api/notes').send()
response.assertOk()
await response.assertJsonPathCount('data', 2)
})
it('should get a note by ID', async () => {
const createResponse = await module.http
.post('/api/notes')
.withBody({ title: 'My Note', content: 'Content' })
.send()
const { data } = await createResponse.json<{ data: { id: string } }>()
const response = await module.http.get(`/api/notes/${data.id}`).send()
response.assertOk()
await response.assertJsonPath('data.id', data.id)
await response.assertJsonPath('data.title', 'My Note')
})
it('should update a note', async () => {
const createResponse = await module.http
.post('/api/notes')
.withBody({ title: 'Original', content: 'Content' })
.send()
const { data } = await createResponse.json<{ data: { id: string } }>()
const response = await module.http
.patch(`/api/notes/${data.id}`)
.withBody({ title: 'Updated' })
.send()
response.assertOk()
await response.assertJsonPath('data.title', 'Updated')
})
it('should delete a note', async () => {
const createResponse = await module.http
.post('/api/notes')
.withBody({ title: 'To Delete', content: 'Gone soon' })
.send()
const { data } = await createResponse.json<{ data: { id: string } }>()
const deleteResponse = await module.http
.delete(`/api/notes/${data.id}`)
.send()
deleteResponse.assertNoContent()
// Verify it's gone
const getResponse = await module.http.get(`/api/notes/${data.id}`).send()
getResponse.assertNotFound()
})
it('should return 404 for non-existent note', async () => {
const response = await module.http
.get('/api/notes/non-existent-id')
.send()
response.assertNotFound()
})
})