create-e2e-test
community[skill]
Create E2E test file for a specified module. Use when adding end-to-end tests for controllers or unit tests for services and repositories.
$
/plugin install coredetails
Create Test
Create test file for a module. Module name: $ARGUMENTS
Test File Locations
- Controller/service tests:
apps/core/test/src/modules/<module-name>/<module-name>.controller.spec.ts - Repository tests (with real PG):
apps/core/test/src/modules/<module-name>/<module-name>.repository.spec.ts
Test Pattern 1: Unit Test with Mocked Dependencies
For testing controllers, services, or business logic without a real database:
import { Test } from '@nestjs/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { <Name>Controller } from '~/modules/<name>/<name>.controller'
import { <Name>Service } from '~/modules/<name>/<name>.service'
describe('<Name>Controller', () => {
let controller: <Name>Controller
const mock<Name>Service = {
findById: vi.fn(),
create: vi.fn(),
update: vi.fn(),
deleteById: vi.fn(),
}
beforeEach(async () => {
vi.clearAllMocks()
const module = await Test.createTestingModule({
controllers: [<Name>Controller],
providers: [
{
provide: <Name>Service,
useValue: mock<Name>Service,
},
],
}).compile()
controller = module.get(<Name>Controller)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
it('calls service.findById with the correct id', async () => {
const mockRow = { id: '1234567890', name: 'Test', createdAt: new Date() }
mock<Name>Service.findById.mockResolvedValue(mockRow)
const result = await controller.getById({ id: '1234567890' })
expect(mock<Name>Service.findById).toHaveBeenCalledWith('1234567890')
expect(result).toEqual(mockRow)
})
})
Test Pattern 2: E2E Test with createE2EApp
For testing controllers through the full HTTP pipeline (with mocked DB):
import { describe, expect, it } from 'vitest'
import { <Name>Controller } from '~/modules/<name>/<name>.controller'
import { <Name>Service } from '~/modules/<name>/<name>.service'
import { <Name>Repository } from '~/modules/<name>/<name>.repository'
import { createE2EApp } from 'test/helper/create-e2e-app'
describe('<Name>Controller (e2e)', () => {
const proxy = createE2EApp({
controllers: [<Name>Controller],
providers: [<Name>Service, <Name>Repository],
})
it('GET /<name>s returns paginated list', async () => {
const res = await proxy.app.inject({
method: 'GET',
url: '/<name>s',
})
expect(res.statusCode).toBe(200)
const json = res.json()
// ResponseInterceptor wraps arrays as { data: [...] }
// Paginated responses include { data: [...], pagination: {...} }
})
it('GET /<name>s/:id returns single item', async () => {
const res = await proxy.app.inject({
method: 'GET',
url: '/<name>s/invalid-id',
})
// Invalid Snowflake IDs should return 400
expect(res.statusCode).toBe(400)
})
})
createE2EApp Behavior
The createE2EApp helper (from test/helper/create-e2e-app.ts):
- Sets up a NestJS testing module with your controllers and providers
- Overrides
AuthGuardwithAuthTestingGuard— pass header'test-token': 1for authenticated requests - Registers all standard interceptors (
ResponseInterceptor,JSONTransformInterceptor,DbQueryInterceptor,HttpCacheInterceptor) - Registers the global Zod validation pipe
- Creates a Fastify-based Nest application
- Returns
{ app }— useproxy.app.inject({ method, url, payload })for HTTP requests - Automatically closes the app in
afterAll
Auth in tests: All requests are treated as unauthenticated by default. To authenticate, pass the test header:
const res = await proxy.app.inject({
method: 'POST',
url: '/<name>s',
payload: { name: 'Test' },
headers: { 'test-token': 1 }, // <-- authenticates as mock admin
})
Test Pattern 3: Repository Test with Real PostgreSQL
For testing repository logic against a real database using testcontainers:
import path from 'node:path'
import { drizzle, type NodePgDatabase } from 'drizzle-orm/node-postgres'
import { migrate } from 'drizzle-orm/node-postgres/migrator'
import { Pool } from 'pg'
import { beforeAll, beforeEach, describe, expect, it, afterAll } from 'vitest'
import { <name>s } from '~/database/schema'
import { <Name>Repository } from '~/modules/<name>/<name>.repository'
import { SnowflakeService } from '~/shared/id/snowflake.service'
const verifyUrl = process.env.PG_VERIFY_URL
const describeIfPg = verifyUrl ? describe : describe.skip
describeIfPg('<Name>Repository', () => {
let pool: Pool
let db: NodePgDatabase<typeof import('~/database/schema')>
let repository: <Name>Repository
let snowflake: SnowflakeService
beforeAll(async () => {
pool = new Pool({ connectionString: verifyUrl })
db = drizzle(pool, { casing: 'snake_case' })
const migrationsFolder = path.resolve(
__dirname,
'../../../../src/database/migrations',
)
await migrate(db, { migrationsFolder })
snowflake = new SnowflakeService()
repository = new <Name>Repository(db as any, snowflake)
}, 60_000)
beforeEach(async () => {
// Truncate tables between tests (respect FK order)
await pool.query('truncate table <name>s cascade')
})
afterAll(async () => {
if (pool) await pool.end()
})
it('creates a row with a generated Snowflake id', async () => {
const created = await repository.create({
name: 'test',
})
expect(typeof created.id).toBe('string')
expect(created.id).toMatch(/^[1-9]\d+$/) // Snowflake format
expect(created.name).toBe('test')
})
it('findById returns null for missing id', async () => {
const result = await repository.findById('9999999999999999999')
expect(result).toBeNull()
})
it('update mutates only specified fields', async () => {
const created = await repository.create({ name: 'old' })
const updated = await repository.update(created.id, { name: 'new' })
expect(updated?.name).toBe('new')
})
it('deleteById removes the row', async () => {
const created = await repository.create({ name: 'to-delete' })
await repository.deleteById(created.id)
const found = await repository.findById(created.id)
expect(found).toBeNull()
})
})
Running with Testcontainers
To run repository tests that need PostgreSQL, start a container first:
# Set PG_VERIFY_URL to a running PostgreSQL instance
export PG_VERIFY_URL=postgres://mx:mx@127.0.0.1:5432/mx_verify
# Or use docker:
docker run -d --name pg-test -e POSTGRES_USER=mx -e POSTGRES_PASSWORD=mx -e POSTGRES_DB=mx_verify -p 5433:5432 postgres:17-alpine
export PG_VERIFY_URL=postgres://mx:mx@127.0.0.1:5433/mx_verify
Then run the test:
# Run a single test file
pnpm test -- test/src/modules/<name>/<name>.repository.spec.ts
# Run all tests
pnpm test
# Watch mode
pnpm -C apps/core run test:watch
Common Assertion Patterns
HTTP Response Assertions
// Status codes
expect(res.statusCode).toBe(200)
expect(res.statusCode).toBe(201) // Created
expect(res.statusCode).toBe(204) // No Content (deletes)
expect(res.statusCode).toBe(400) // Validation error
expect(res.statusCode).toBe(404) // Not found
// JSON body (auto snake_case via JSONTransformInterceptor)
const json = res.json()
expect(json).toMatchObject({ name: 'test' })
// Paginated response shape
expect(json).toMatchObject({
data: expect.any(Array),
pagination: expect.objectContaining({
total: expect.any(Number),
current_page: expect.any(Number),
total_page: expect.any(Number),
}),
})
Snowflake ID Assertions
// Valid Snowflake ID format
expect(created.id).toMatch(/^[1-9]\d+$/)
expect(typeof created.id).toBe('string')
// Invalid ID throws
await expect(repository.findById('not-an-id')).rejects.toThrow()
Direct DB Inserts for Test Setup
When testing with a real database, use Drizzle directly to set up test data:
await db.insert(<name>s).values([
{ id: snowflake.nextId(), name: 'Item 1' },
{ id: snowflake.nextId(), name: 'Item 2' },
])
Test Helpers Location
test/helper/create-e2e-app.ts— E2E app setup with mock auth and Redistest/helper/setup-e2e.ts— Low-level NestJS testing module setuptest/helper/pg-testcontainer.ts— PostgreSQL 17 testcontainers helpertest/helper/redis-mock.helper.ts— In-memory Redis mocktest/mock/guard/auth.guard.ts—AuthTestingGuard(bypasses auth withtest-tokenheader)test/mock/modules/— Module-level mocks
Notes
- The
createE2EApphelper uses mock Redis and bypasses auth. For real DB tests, use the repository test pattern. - Use
proxy.app.inject()to send HTTP requests through the full interceptor chain. - Responses go through
JSONTransformInterceptor— field names become snake_case. - Paginated responses include
dataandpaginationfields. - Empty delete responses return 204 status code.
- Repository tests with real PG are gated behind
process.env.PG_VERIFY_URL— usedescribeIfPg/describe.skippattern so they don't fail in CI without a database.
technical
- github
- mx-space/core
- stars
- 530
- license
- NOASSERTION
- contributors
- 33
- last commit
- 2026-05-29T06:39:36Z
- file
- .claude/skills/create-e2e-test/SKILL.md