Testing utilities and mocks for Stratal framework applications.
npm install -D @stratal/testing
# or
yarn add -D @stratal/testing
npm install -D stratal vitest
Set up base modules once in your Vitest setup file, then create test modules in each test:
// vitest.setup.ts
import 'reflect-metadata'
import { Test } from '@stratal/testing'
import { CoreModule } from './src/core.module'
Test.setBaseModules([CoreModule])
// users/__tests__/users.spec.ts
import { Test, type TestingModule } from '@stratal/testing'
import { UsersModule } from '../users.module'
describe('UsersController', () => {
let module: TestingModule
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [UsersModule],
}).compile()
})
afterAll(async () => {
await module.close()
})
it('lists users', async () => {
const response = await module.http
.get('/api/users')
.send()
response.assertOk()
})
})
TestStatic entry point for creating testing modules.
import { Test } from '@stratal/testing'
// Set once in vitest.setup.ts — included in every test module
Test.setBaseModules([CoreModule])
// Create a test module in each test file
const builder = Test.createTestingModule({
imports: [UsersModule, AuthModule],
providers: [{ provide: MOCK_TOKEN, useValue: mockValue }],
controllers: [TestController],
env: { DATABASE_URL: 'test://db' },
})
TestingModuleBuilderFluent builder returned by Test.createTestingModule(). Chain provider overrides, then call .compile().
const module = await Test.createTestingModule({
imports: [OrdersModule],
})
.overrideProvider(PAYMENT_TOKEN)
.useValue(mockPaymentService)
.withEnv({ STRIPE_KEY: 'sk_test_xxx' })
.compile()
TestingModuleThe compiled test context. Provides access to services, HTTP client, storage, and lifecycle management.
// Resolve services from the DI container
const service = module.get(ORDER_TOKENS.OrderService)
// Access the HTTP test client
module.http
// Access fake storage for assertions
module.storage
// Execute code in a request-scoped container
await module.runInRequestScope(async () => {
const scoped = module.get(REQUEST_SCOPED_TOKEN)
})
// Cleanup in afterAll
await module.close()
The built-in HTTP client provides a fluent API for making requests and asserting responses.
const response = await module.http
.forHost('api.example.com') // optional — defaults to localhost
.post('/api/v1/orders')
.withHeaders({ Authorization: 'Bearer token' })
.withBody({ item: 'Widget', qty: 3 })
.send()
All HTTP methods are supported: .get(), .post(), .put(), .patch(), .delete().
response
.assertCreated() // 201
.assertHeader('Content-Type', 'application/json')
// Status helpers
response.assertOk() // 200
response.assertNoContent() // 204
response.assertBadRequest() // 400
response.assertUnauthorized() // 401
response.assertForbidden() // 403
response.assertNotFound() // 404
response.assertUnprocessable() // 422
response.assertServerError() // 500
response.assertStatus(418) // any status
response.assertSuccessful() // 2xx range
// JSON assertions (async — chainable with await)
await response.assertJson({ success: true })
await response.assertJsonPath('data.user.id', expect.any(String))
await response.assertJsonPaths({ 'data.name': 'Alice', 'data.role': 'admin' })
await response.assertJsonStructure(['id', 'name', 'email'])
await response.assertJsonPathExists('data.createdAt')
await response.assertJsonPathMissing('data.password')
await response.assertJsonPathCount('data.items', 3)
await response.assertJsonPathContains('data.bio', 'engineer')
await response.assertJsonPathIncludes('data.tags', 'featured')
// Header assertions
response.assertHeader('X-Request-Id') // exists
response.assertHeader('X-Request-Id', '123') // exact value
response.assertHeaderMissing('X-Debug')
// Raw access
const json = await response.json<OrderResponse>()
const text = await response.text()
response.status // number
response.headers // Headers
response.raw // underlying Response
Replace any provider in the DI container for testing:
const module = await Test.createTestingModule({
imports: [NotificationModule],
})
// Static value
.overrideProvider(EMAIL_TOKEN)
.useValue(mockEmailService)
// Class replacement
.overrideProvider(LOGGER_TOKEN)
.useClass(SilentLogger)
// Factory with container access
.overrideProvider(CACHE_TOKEN)
.useFactory((container) => new InMemoryCache(container.resolve(CONFIG_TOKEN)))
// Alias to existing token
.overrideProvider(PAYMENT_TOKEN)
.useExisting(MOCK_PAYMENT_TOKEN)
.compile()
Mock external HTTP calls with createFetchMock(), backed by undici MockAgent:
import { createFetchMock, type FetchMock } from '@stratal/testing'
describe('GeoService', () => {
let module: TestingModule
let fetchMock: FetchMock
beforeEach(() => {
fetchMock = createFetchMock()
fetchMock.activate()
fetchMock.disableNetConnect()
})
afterEach(() => {
fetchMock.reset()
})
it('looks up coordinates', async () => {
fetchMock.mockJsonResponse('https://geo.api.com/lookup', {
lat: 40.7128,
lng: -74.006,
})
const response = await module.http
.get('/api/geo/lookup?address=NYC')
.send()
response.assertOk()
await response.assertJsonPath('data.lat', 40.7128)
fetchMock.assertNoPendingInterceptors()
})
it('handles API errors', async () => {
fetchMock.mockError('https://geo.api.com/lookup', 503, 'Service Unavailable')
const response = await module.http
.get('/api/geo/lookup?address=NYC')
.send()
response.assertServerError()
})
})
For advanced scenarios, access the undici MockPool directly:
fetchMock
.get('https://api.example.com')
.intercept({ path: '/users', method: 'POST' })
.reply(201, { id: '1' })
FakeStorageService is an in-memory storage implementation auto-registered in every test module. It replaces the real storage service and provides assertion helpers.
it('uploads a document', async () => {
await module.http
.post('/api/documents')
.withBody({ name: 'report.pdf' })
.send()
// Assert files were stored
module.storage.assertExists('documents/report.pdf')
module.storage.assertMissing('documents/old.pdf')
module.storage.assertCount(1)
// Inspect stored files
const file = module.storage.getFile('documents/report.pdf')
expect(file?.mimeType).toBe('application/pdf')
expect(file?.size).toBeGreaterThan(0)
})
afterEach(() => {
module.storage.clear() // reset between tests
})
Create deeply-mocked instances of any interface or class with createMock() from @golevelup/ts-vitest:
import { createMock, type DeepMocked } from '@stratal/testing/mocks'
let mockService: DeepMocked<PaymentService>
beforeEach(() => {
mockService = createMock<PaymentService>()
mockService.charge.mockResolvedValue({ id: 'ch_123', status: 'paid' })
})
A drop-in mock for nodemailer, useful in Vitest's module mocking:
// vitest.config.ts (or inline vi.mock)
export default defineConfig({
test: {
alias: {
nodemailer: '@stratal/testing/mocks/nodemailer',
},
},
})
import { Test, TestingModule, createFetchMock } from '@stratal/testing'
import { createMock, type DeepMocked } from '@stratal/testing/mocks'
import nodemailer from '@stratal/testing/mocks/nodemailer'
MIT