Skip to content

Mocks and Fakes

@stratal/testing provides several tools for isolating your code from external dependencies: deep mocks with createMock, an in-memory FakeStorageService, fetch interception with MockFetch (built on MSW), and a nodemailer mock for email testing.

createMock<T>() generates a deeply mocked object where every method is replaced with a Vitest spy. It is re-exported from @golevelup/ts-vitest and available from @stratal/testing/mocks:

import { createMock, type DeepMocked } from '@stratal/testing/mocks'
import type { EmailService } from 'stratal/email'
const mockEmailService = createMock<EmailService>()
// Every method is a Vitest spy
mockEmailService.send.mockResolvedValueOnce(undefined)
// Verify calls
expect(mockEmailService.send).toHaveBeenCalledOnce()

Combine createMock with .overrideProvider() to replace real services in your test module:

import { Test, type TestingModule } from '@stratal/testing'
import { createMock, type DeepMocked } from '@stratal/testing/mocks'
import { EMAIL_TOKENS, type EmailService } from 'stratal/email'
let module: TestingModule
let mockEmailService: DeepMocked<EmailService>
beforeEach(async () => {
mockEmailService = createMock<EmailService>()
module = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(EMAIL_TOKENS.EmailService)
.useValue(mockEmailService)
.compile()
})

Every test module automatically registers a FakeStorageService that replaces the real StorageService. It stores files in memory and provides assertion helpers for verifying upload and delete operations.

FakeStorageService extends StorageService and uses an in-memory Map instead of S3. All storage operations (upload, download, delete, exists, presigned URLs) work without real cloud credentials. It is registered as a singleton in the test container so files uploaded during setup are available when consumers run.

Access it through module.storage:

module.storage.assertExists('uploads/avatar.png')
module.storage.assertMissing('uploads/deleted-file.pdf')

Each file in the fake storage is represented by a StoredFile object:

PropertyTypeDescription
contentUint8ArrayFile content as bytes
mimeTypestringMIME type (e.g., application/pdf)
sizenumberFile size in bytes
metadataRecord<string, string> | undefinedOptional custom metadata
uploadedAtDateTimestamp when the file was stored
MethodAsserts
assertExists(path)A file exists at the given path
assertMissing(path)No file exists at the given path
assertEmpty()Storage contains no files
assertCount(count)Storage contains exactly count files
MethodReturnsDescription
getStoredFiles()Map<string, StoredFile>All stored files
getStoredPaths()string[]All file paths
getFile(path)StoredFile | undefinedA specific file by path
clear()voidRemove all files

Here is a complete example testing file upload and deletion:

import { Test, type TestingModule } from '@stratal/testing'
import { afterAll, beforeEach, describe, it } from 'vitest'
import { DocumentsModule } from '../documents.module'
describe('Document upload', () => {
let module: TestingModule
beforeEach(async () => {
module = await Test.createTestingModule({
imports: [DocumentsModule],
}).compile()
module.storage.clear()
})
afterAll(async () => {
await module.close()
})
it('should upload a document', async () => {
const response = await module.http
.post('/api/documents')
.withBody({ name: 'report.pdf', content: 'base64data...' })
.send()
response.assertCreated()
module.storage.assertCount(1)
module.storage.assertExists('documents/report.pdf')
})
it('should delete a document', async () => {
// Upload first
await module.http
.post('/api/documents')
.withBody({ name: 'temp.pdf', content: 'base64data...' })
.send()
module.storage.assertExists('documents/temp.pdf')
// Delete
await module.http.delete('/api/documents/temp.pdf').send()
module.storage.assertMissing('documents/temp.pdf')
})
})

createMockFetch() creates a fetch interceptor built on MSW (Mock Service Worker). Use it to intercept outgoing fetch calls in your tests without hitting real external APIs.

import { createMockFetch } from '@stratal/testing'
const mock = createMockFetch()

Start intercepting in beforeAll, reset handlers between tests in afterEach, and stop intercepting in afterAll:

import { createMockFetch } from '@stratal/testing'
import { afterAll, afterEach, beforeAll } from 'vitest'
const mock = createMockFetch()
beforeAll(() => {
mock.listen()
})
afterEach(() => {
mock.reset()
})
afterAll(() => {
mock.close()
})

Use mockJsonResponse() to quickly set up a mock for a JSON API endpoint:

it('should fetch user data from external API', async () => {
mock.mockJsonResponse('https://api.example.com/users/1', {
id: 1,
name: 'Alice',
})
const response = await module.http.get('/api/users/1/profile').send()
response.assertOk()
await response.assertJsonPath('data.name', 'Alice')
})

mockJsonResponse accepts an options object for customization:

OptionTypeDefaultDescription
statusnumber200HTTP status code
headersRecord<string, string>{}Additional response headers
delaynumberResponse delay in milliseconds
methodstring'GET'HTTP method to match
pathstringOverride the URL pathname
mock.mockJsonResponse(
'https://api.example.com/users',
{ created: true },
{ method: 'POST', status: 201 }
)

Use mockError() to simulate error responses from external APIs:

it('should handle external API failure', async () => {
mock.mockError('https://api.example.com/users/1', 503, 'Service Unavailable')
const response = await module.http.get('/api/users/1/profile').send()
response.assertServerError()
})

mockError accepts an options object:

OptionTypeDefaultDescription
headersRecord<string, string>{}Additional response headers
methodstring'GET'HTTP method to match
pathstringOverride the URL pathname

Use mock.use() to add MSW request handlers for a single test. These handlers are cleared on mock.reset():

import { createMockFetch, http, HttpResponse } from '@stratal/testing'
const mock = createMockFetch()
it('should handle a custom response', async () => {
mock.use(
http.get('https://api.example.com/status', () => {
return HttpResponse.json({ status: 'degraded' }, { status: 503 })
})
)
const response = await module.http.get('/api/health').send()
response.assertServerError()
})
MethodDescription
listen()Start intercepting HTTP requests. Call in beforeAll.
reset()Reset runtime handlers. Call in afterEach.
close()Stop intercepting. Call in afterAll.
use(...handlers)Add runtime MSW request handlers for a single test
mockJsonResponse(url, data, options?)Mock a JSON response
mockError(url, status, message?, options?)Mock an error response

If your application uses the SMTP email provider, stratalTest() automatically aliases nodemailer to @stratal/testing/mocks/nodemailer — no extra configuration needed.

The mock replaces createTransport with a no-op that resolves sendMail calls immediately. No emails are sent and no SMTP connection is made.

Use getTestEnv() for standalone tests that need a StratalEnv object without bootstrapping a full module:

import { getTestEnv } from '@stratal/testing'
it('should format config from env', () => {
const env = getTestEnv({ APP_NAME: 'test-app' })
expect(env.APP_NAME).toBe('test-app')
})

getTestEnv() merges the cloudflare:test environment with your overrides, giving you a complete StratalEnv object.

Here is a combined example using provider overrides, fetch mocking, and storage assertions in a single test suite:

import { Test, type TestingModule, createMockFetch } from '@stratal/testing'
import { createMock, type DeepMocked } from '@stratal/testing/mocks'
import { EMAIL_TOKENS, type EmailService } from 'stratal/email'
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from 'vitest'
import { InvoiceModule } from '../invoice.module'
describe('InvoiceController', () => {
let module: TestingModule
let mockEmailService: DeepMocked<EmailService>
const fetchMock = createMockFetch()
beforeAll(() => {
fetchMock.listen()
})
beforeEach(async () => {
mockEmailService = createMock<EmailService>()
module = await Test.createTestingModule({
imports: [InvoiceModule],
})
.overrideProvider(EMAIL_TOKENS.EmailService)
.useValue(mockEmailService)
.compile()
module.storage.clear()
})
afterEach(() => {
fetchMock.reset()
})
afterAll(async () => {
fetchMock.close()
await module.close()
})
it('should generate an invoice, store the PDF, and send email', async () => {
// Mock external tax API
fetchMock.mockJsonResponse('https://tax.example.com/calculate', {
tax: 1800,
total: 11800,
}, { method: 'POST' })
const response = await module.http
.post('/api/invoices')
.withBody({
customer: 'alice@example.com',
items: [{ name: 'Widget', price: 10000, quantity: 1 }],
})
.send()
response.assertCreated()
await response.assertJsonPath('data.total', 11800)
// Verify PDF was stored
module.storage.assertCount(1)
// Verify email was sent
expect(mockEmailService.send).toHaveBeenCalledOnce()
})
})