Japa Testing Skill
AdonisJS uses Japa — a backend testing framework built and maintained by the AdonisJS team. Unlike Jest/Vitest, Japa runs natively in Node.js without transpilers and has plugins specifically designed for backend testing.
Suites and structure
tests/
├── bootstrap.ts # plugins, hooks, suite config
├── unit/ # fast in-process tests
└── browser/ # end-to-end with Playwright
Suites defined in adonisrc.ts:
tests: {
suites: [
{ files: ['tests/unit/**/*.spec.ts'], name: 'unit', timeout: 2000 },
{ files: ['tests/browser/**/*.spec.ts'], name: 'browser', timeout: 300000 },
],
forceExit: false,
}
bootstrap.ts
Hypermedia/Inertia starter kits (browser + unit suites):
import { assert } from '@japa/assert'
import app from '@adonisjs/core/services/app'
import type { Config } from '@japa/runner/types'
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
import testUtils from '@adonisjs/core/services/test_utils'
import { browserClient } from '@japa/browser-client'
import { authBrowserClient } from '@adonisjs/auth/plugins/browser_client'
import { sessionBrowserClient } from '@adonisjs/session/plugins/browser_client'
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
browserClient({ runInSuites: ['browser'] }),
sessionBrowserClient(app),
authBrowserClient(app),
]
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
setup: [() => testUtils.db().migrate()],
teardown: [],
}
export const configureSuite: Config['configureSuite'] = (suite) => {
if (['browser', 'functional', 'e2e'].includes(suite.name)) {
return suite.setup(() => testUtils.httpServer().start())
}
}
API starter kit (adds apiClient plugins):
import { apiClient } from '@japa/api-client'
import { authApiClient } from '@adonisjs/auth/plugins/api_client'
import { sessionApiClient } from '@adonisjs/session/plugins/api_client'
import type { Registry } from '../.adonisjs/client/registry/schema.d.ts'
declare module '@japa/api-client/types' {
interface RoutesRegistry extends Registry {}
}
export const plugins: Config['plugins'] = [
assert(),
pluginAdonisJS(app),
apiClient(),
sessionApiClient(app),
authApiClient(app),
]
.env.test — always required for auth in tests:
SESSION_DRIVER=memory
Running tests
node ace test # all suites
node ace test unit # specific suite
node ace test browser
node ace test --files="posts/index" # specific file
node ace test --files="posts/*" # folder wildcard
node ace test --tests="can list posts" # exact test title
node ace test --groups="Posts index" # group name
node ace test --watch # re-run on change
node ace test --bail # stop on first failure
node ace test --failed # re-run only failed tests
Database state — between each test
Choose one approach per group:
// Global transactions (faster — rolls back, nothing persisted)
group.each.setup(() => testUtils.db().withGlobalTransaction())
// Truncation (actual delete — slower but more compatible)
group.each.setup(() => testUtils.db().truncate())
Run migrations once before all tests in bootstrap.ts:
runnerHooks.setup = [
() => testUtils.db().migrate(),
() => testUtils.db().seed(), // optional
]
Multiple connections:
() => testUtils.db().migrate()
() => testUtils.db('tenant').migrate()
References
| Topic | File |
|---|---|
| API tests — client.visit(), assertions, auth | references/api-tests.md |
| Browser tests — Playwright, forms, loginAs, cookies | references/browser-tests.md |
| Console tests — Ace commands, raw mode, prompts | references/console-tests.md |
| Test doubles — fakes, swaps, time, sinon | references/test-doubles.md |
Runbooks
| Feature | File |
|---|---|
| Full test suite for a CRUD resource | runbooks/resource-tests.md |
Anti-Patterns
| Wrong | Correct |
|---|---|
| No DB cleanup between tests | withGlobalTransaction() or truncate() in group.each.setup |
SESSION_DRIVER not set to memory |
Required in .env.test for loginAs to work |
Using fakes without using or .restore() |
using fake = mail.fake() for auto-restore |
sinon.restore() in group.each.setup |
sinon.restore() in group.each.teardown |
redis.flushdb() in group.each.setup |
In group.each.teardown (clear after, not before) |
| Mocking DB queries | Hit the real test DB — mocks miss constraint errors |
record() in committed test |
Only for generating code — replace with visit() |
| Prompt trap without exact title match | Titles are case-sensitive — copy exactly from command |