Appearance
Testing with Japa
The server uses Japa as its test runner — not Vitest or Jest. Tests are co-located in apps/server/tests/.
Test Suites
Two suites are defined in apps/server/adonisrc.ts:
| Suite | Pattern | Timeout | When to use |
|---|---|---|---|
unit | tests/unit/**/*.spec.ts | 2 s | Pure logic, no HTTP, no DB |
functional | tests/functional/**/*.spec.ts | 30 s | Full HTTP stack with DB |
Functional tests automatically start the AdonisJS HTTP server via testUtils.httpServer().start() (configured in tests/bootstrap.ts).
Running Tests
All commands run from apps/server/:
bash
# Run all suites
node ace test
# Run a specific suite
node ace test --suite unit
node ace test --suite functional
# Run a single file
node ace test --files tests/functional/events.spec.ts
# Run multiple files (comma-separated, no spaces)
node ace test --files "tests/unit/a.spec.ts,tests/unit/b.spec.ts"Japa does not support
--coverage,--watch, or--grepflags. Use--filesto narrow scope.
Bootstrap & Plugins
tests/bootstrap.ts configures three Japa plugins registered for every suite:
typescript
import { apiClient } from '@japa/api-client'
import { assert } from '@japa/assert'
import { pluginAdonisJS } from '@japa/plugin-adonisjs'
export const plugins = [assert(), apiClient(), pluginAdonisJS(app)]| Plugin | What it provides |
|---|---|
@japa/assert | assert object in test context — assert.equal(), etc. |
@japa/api-client | client object — makes HTTP requests to the running server |
@japa/plugin-adonisjs | Integrates AdonisJS DI container and test utilities |
The configureSuite hook starts the HTTP server for functional (and browser/e2e) suites only:
typescript
export const configureSuite = (suite) => {
if (['browser', 'functional', 'e2e'].includes(suite.name)) {
return suite.setup(() => testUtils.httpServer().start())
}
}Writing Tests
Unit Test
typescript
// tests/unit/event_service.spec.ts
import { test } from '@japa/runner'
test.group('EventService', () => {
test('calculates something pure', ({ assert }) => {
const result = 1 + 1
assert.equal(result, 2)
})
})Functional Test (HTTP)
typescript
// tests/functional/events.spec.ts
import EventFactory from '#tests/factories/event'
import { test } from '@japa/runner'
test.group('Events API', () => {
test('returns 200 for a valid event', async ({ client }) => {
const event = await EventFactory.with('organization').create()
const response = await client.get(`/organizations/${event.organizationId}/events/${event.id}`)
response.assertStatus(200)
response.assertBodyContains({ data: { id: event.id } })
})
test('returns 401 when unauthenticated', async ({ client }) => {
const response = await client.get('/organizations/fake-id/events')
response.assertStatus(401)
})
})Factories
Factories live in tests/factories/ and use @adonisjs/lucid/factories. They create model instances with realistic fake data via Faker.
User Factory
typescript
// tests/factories/user.ts
import User from '#models/user'
import factory from '@adonisjs/lucid/factories'
const UserFactory = factory
.define(User, ({ faker }) => ({
email: faker.internet.email(),
name: faker.person.fullName(),
emailVerified: true,
image: faker.helpers.maybe(() => faker.image.avatar(), {
probability: 0.3,
}),
}))
.state('verified', (user) => {
user.emailVerified = true
return user
})
.state('unverified', (user) => {
user.emailVerified = false
return user
})
.build()
export default UserFactoryEvent Factory
typescript
// tests/factories/event.ts
import Event from '#models/event'
import { EventStatus, EventType } from '#models/event'
import factory from '@adonisjs/lucid/factories'
import OrganizationFactory from './organization.js'
const EventFactory = factory
.define(Event, ({ faker }) => ({
organizationId: faker.string.uuid(),
name: faker.company.catchPhrase(),
status: EventStatus.BACKLOG,
eventType: EventType.ONE_OFF,
metadata: {},
}))
.relation('organization', () => OrganizationFactory) // creates a real org in DB
.state('draft', (event) => {
event.status = EventStatus.BACKLOG
return event
})
.state('planned', (event) => {
event.status = EventStatus.PLANNED
return event
})
.state('recurring', (event) => {
event.eventType = EventType.RECURRING
event.recurrenceRule = 'FREQ=WEEKLY;BYDAY=MO,WE,FR;COUNT=10'
return event
})
.build()
export default EventFactoryAvailable Factories
All factories are in apps/server/tests/factories/:
| File | Model |
|---|---|
user.ts | User |
organization.ts | Organization |
member.ts | Member |
event.ts | Event |
event_plan.ts | EventPlan |
event_block.ts | EventBlock |
event_task.ts | EventTask |
event_task_assignee.ts | EventTaskAssignee |
event_budget_item.ts | EventBudgetItem |
event_comment.ts | EventComment |
event_collaborator.ts | EventCollaborator |
event_personnel.ts | EventPersonnel |
event_permission.ts | EventPermission |
Using Factories
typescript
// Create a single instance (persisted to DB)
const user = await UserFactory.create()
// Create with state
const unverified = await UserFactory.states('unverified').create()
// Create with relations (creates org first, then event referencing it)
const event = await EventFactory.with('organization').create()
// Create multiple
const events = await EventFactory.createMany(5)
// Create without persisting (model instance only)
const stub = await UserFactory.make()No Spec Files Yet
⚠️ WARNING
As of March 2026, no .spec.ts files exist in tests/unit/ or tests/functional/. The factories and test infrastructure are fully set up, but no test cases have been written yet. Running node ace test exits cleanly with 0 tests.
See Also
- AdonisJS Commands —
node ace testflags - Database Models — model patterns used in factories
- Japa documentation — official runner reference