- name
- kickjs-write-controller-test
- description
- Use when adding a Vitest test that exercises an HTTP route or DI graph.
<!-- Generated by `kick g agents` for kick-fullstack-server. Edits are overwritten on the next refresh; keep customisation in a SKILL.local.md alongside. -->
**Template** (copy/paste, adjust):
```ts
import { describe, it, expect, beforeEach } from 'vitest'
import request from 'supertest'
import { Container } from '@forinda/kickjs'
import { createTestApp } from '@forinda/kickjs-testing'
// Side-effect import — registers the env schema, exactly as src/index.ts does.
// `createTestApp` never loads the entry file, so without this
// `ConfigService.get('YOUR_KEY')` is undefined under test while `@Value()`
// still appears to work via its process.env fallback — the two disagree only
// in tests.
import '@/config'
describe('UserController', () => {
beforeEach(() => Container.reset()) // isolate DI between tests
it('returns users', async () => {
// `createTestApp` takes an OPTIONS OBJECT and returns
// `{ app, container }`. Passing a bare array throws
// "this.options.modules is not iterable"; the result has no `.get()`.
//
// Drive `app.handle` rather than `expressApp`: it is the Application's
// own Node listener, so the test runs on whichever runtime the app is
// configured with. Pass `runtime` to `createTestApp` to match production.
const { app } = await createTestApp({
// Matches this project's `modules.style`.
modules: [UserModule()],
})
const res = await request(app.handle.bind(app)).get('/api/v1/users')
expect(res.status).toBe(200)
})
})
```
**Typed handler signature** — pair with `kick typegen` so `ctx.body` / `params` / `query` are typed by the route's Zod schema:
```ts
@Post('/', { body: createTodoSchema })
async create(ctx: Ctx<KickRoutes.TodoController['create']>) {
// ctx.body is typed from createTodoSchema; ctx.params from the route.
// Returning (vs ctx.created) lets typegen infer the response type.
return reply(201, await this.service.create(ctx.body))
}
```
**Red flags**:
- `new Container()` — wrong; use `Container.reset()` in `beforeEach` or `Container.create()` for fully isolated graphs.
- `Container.getInstance().reset()` — wrong; same fix.
- Sharing a container instance across `it()` blocks — leaks registrations between tests.
- Injecting a `Scope.REQUEST` service into a `SINGLETON` — container throws at resolve. Singletons must resolve request-scoped services explicitly per call.
- Calling `getRequestValue<string>('traceId')` — the generic slot is the **key** type, not the value type; widens key and bypasses typed lookup.
- Asserting on `res.body.requestId` when `requestId()` middleware isn't mounted in the test app — value will be `undefined`.
- Using `Scope.REQUEST` services in a test without mounting `requestScopeMiddleware()` — `getRequestValue` silently returns `undefined`; `getRequestStore` throws.
**Nuances**:
- `@Inject` and `@Autowired` are interchangeable — same runtime, same types; pick by readability.
- `@Value('MISSING_KEY')` with no default **throws on property access**, not at construction — tests that exercise the getter will surface the missing-env issue.
Voir sur GitHub