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.
Deep mocks with createMock
Section titled “Deep mocks with createMock”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 spymockEmailService.send.mockResolvedValueOnce(undefined)
// Verify callsexpect(mockEmailService.send).toHaveBeenCalledOnce()Using mocks with provider overrides
Section titled “Using mocks with provider overrides”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: TestingModulelet mockEmailService: DeepMocked<EmailService>
beforeEach(async () => { mockEmailService = createMock<EmailService>()
module = await Test.createTestingModule({ imports: [AppModule], }) .overrideProvider(EMAIL_TOKENS.EmailService) .useValue(mockEmailService) .compile()})FakeStorageService
Section titled “FakeStorageService”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.
How it works
Section titled “How it works”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')StoredFile interface
Section titled “StoredFile interface”Each file in the fake storage is represented by a StoredFile object:
| Property | Type | Description |
|---|---|---|
content | Uint8Array | File content as bytes |
mimeType | string | MIME type (e.g., application/pdf) |
size | number | File size in bytes |
metadata | Record<string, string> | undefined | Optional custom metadata |
uploadedAt | Date | Timestamp when the file was stored |
Assertion helpers
Section titled “Assertion helpers”| Method | Asserts |
|---|---|
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 |
Inspection methods
Section titled “Inspection methods”| Method | Returns | Description |
|---|---|---|
getStoredFiles() | Map<string, StoredFile> | All stored files |
getStoredPaths() | string[] | All file paths |
getFile(path) | StoredFile | undefined | A specific file by path |
clear() | void | Remove 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') })})FetchMock
Section titled “FetchMock”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()Setup and teardown
Section titled “Setup and teardown”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()})Mocking JSON responses
Section titled “Mocking JSON responses”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:
| Option | Type | Default | Description |
|---|---|---|---|
status | number | 200 | HTTP status code |
headers | Record<string, string> | {} | Additional response headers |
delay | number | — | Response delay in milliseconds |
method | string | 'GET' | HTTP method to match |
path | string | — | Override the URL pathname |
mock.mockJsonResponse( 'https://api.example.com/users', { created: true }, { method: 'POST', status: 201 })Mocking errors
Section titled “Mocking errors”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:
| Option | Type | Default | Description |
|---|---|---|---|
headers | Record<string, string> | {} | Additional response headers |
method | string | 'GET' | HTTP method to match |
path | string | — | Override the URL pathname |
Adding runtime handlers with use()
Section titled “Adding runtime handlers with use()”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()})MockFetch methods reference
Section titled “MockFetch methods reference”| Method | Description |
|---|---|
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 |
Nodemailer mock
Section titled “Nodemailer mock”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.
Environment helpers
Section titled “Environment helpers”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.
Putting it all together
Section titled “Putting it all together”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() })})Next steps
Section titled “Next steps”- Testing Module for creating modules and provider overrides.
- HTTP Testing for the full request builder and response assertion API.
- Testing Overview for installation and project setup.
- Storage for the production storage module configuration.