Skip to content

Testing ​

KickJS apps are tested with Vitest. @forinda/kickjs-testing builds your app for a test without opening a port, sends requests through the runtime you deploy on, and replaces dependencies through DI. Database code has its own helpers in @forinda/kickjs-db/testing.

What's in the box ​

Package / helperFor
createTestApp, client()building the app and sending requests — HTTP Integration Tests
useTestApp (@forinda/kickjs-testing/vitest)one app per file or per worker, set up and torn down for you
onTestReset, resetTestStateclearing fakes and caches between files — Large Suites
runContributor, createTestPlugin, createTestModulecontributors, plugins and hand-wired modules in isolation — Contributors, Middleware and Plugins
withEnv (@forinda/kickjs)different config values for one test — Test Environment
createTestDb, createPgTestDb, rolledBack (@forinda/kickjs-db/testing)a database per file, a rolled-back transaction per test — Testing with kick/db
createTestClient (@forinda/kickjs-client)typed, in-process requests to a createWebApp app — Typed Client

Setup ​

An app made with kick new is ready to test: pnpm test runs Vitest (so does kick test, a command the scaffolded kick.config.ts defines). To add testing to an existing app:

pnpm add -D @forinda/kickjs-testing vitest supertest @types/supertest unplugin-swc

Decorators need TypeScript's legacy decorator metadata, which esbuild and oxc don't emit, so tests compile through SWC. The scaffold's vite.config.ts already does this, and vitest.config.ts merges it rather than repeating it:

ts
// vite.config.ts
import swc from 'unplugin-swc'
import { defineConfig } from 'vite'

export default defineConfig({
  oxc: false, // SWC compiles TypeScript instead
  plugins: [swc.vite()],
})
ts
// vitest.config.ts
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config.ts'

export default mergeConfig(
  viteConfig,
  defineConfig({
    test: { globals: true, environment: 'node', include: ['src/**/*.test.ts'] },
  }),
)

tsconfig.json needs "experimentalDecorators": true and "emitDecoratorMetadata": true. reflect-metadata is imported by @forinda/kickjs itself, so test files don't import it. Put test config in .env.test (Test Environment).

Scaffold them

kick g module <name> writes a controller test (built with createTestApp) and a repository test next to the module. kick g test <name> writes an empty test file with Container.reset() already in beforeEach. Flags: Generators.

Tests live next to the code they test, in __tests__/ folders: src/modules/users/__tests__/user.controller.test.ts.

Which test for what ​

To checkWriteGuide
a business rule: pricing, permissions, state changesa unit test of the service, with fakesUnit Tests
an endpoint: routing, validation, status codes, the response bodyan integration test through client()HTTP Integration Tests
who can call whatan integration test with tokens or a test userTesting Authentication
the queries a repository runsa test against a real databaseTesting with kick/db
a job, a cron job, a socket handler, outgoing mailrun the handler; record what leavesJobs, Cron, WebSockets and Mail
a contributor, a middleware, an adapter, a pluginits helper, plus one integration testContributors, Middleware and Plugins

Most of a suite should be unit tests of the code that decides things, with integration tests proving each endpoint is wired up and enforces its rules. Keep end-to-end tests (a browser, the deployed stack) for a few critical journeys.

A first test ​

ts
// src/modules/users/__tests__/user.controller.test.ts
import { expect, it } from 'vitest'
import { useTestApp } from '@forinda/kickjs-testing/vitest'
import { UserModule } from '../user.module'

const t = useTestApp(() => ({ modules: [UserModule()] }), { client: { basePath: '/api/v1' } })

it('creates a user, then finds it', async () => {
  const created = await t.client().post('/users').send({ email: 'ada@x.io' }).expect(201)
  const found = await t.client().get(`/users/${created.body.id}`).expect(200)
  expect(found.body.email).toBe('ada@x.io')
})

That's the real UserModule (controller, service, repository) answering real HTTP on the default runtime. HTTP Integration Tests goes on from here.

Guides ​

Feature guides with their own testing notes: Asset Manager (fixtures and the asset cache), AI (a scripted provider), MCP (tool tests and the MCP Inspector), Context Decorators (contributor recipes).

Released under the MIT License. Built with TypeScript — runs on Express, Fastify, or h3.