Appearance
Testing with Japa
The server uses Japa for unit and functional tests in apps/server/tests/. Adapter-level tests, including the Lucid Better Auth adapter suite, use Vitest through pnpm run test:adapter.
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 testUtils from '@adonisjs/core/services/test_utils'
import { test } from '@japa/runner'
import { createTestUserWithOrg } from '../helpers/auth.js'
test.group('Events API', (group) => {
group.each.setup(() => testUtils.db().truncate())
test('returns 200 for a valid event', async ({ client }) => {
const { organization, headers } = await createTestUserWithOrg()
const createResponse = await client
.post(`/organizations/${organization.id}/events`)
.headers(headers)
.json({ name: 'Community Dinner' })
const eventId = createResponse.body().data.id
const response = await client
.get(`/organizations/${organization.id}/events/${eventId}`)
.headers(headers)
response.assertStatus(200)
response.assertBodyContains({ data: { id: eventId } })
})
test('returns 401 when unauthenticated', async ({ client }) => {
const response = await client.get('/organizations/fake-id/events/fake-event-id')
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 factory from '@adonisjs/lucid/factories'
import User from '#models/user'
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 factory from '@adonisjs/lucid/factories'
import Event, { EventStatus, EventType } from '#models/event'
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()Current Coverage
The repository contains active Japa unit and functional suites. Current coverage includes events, event plans, newsletter subscriptions, health checks, validators, transformers, models, repositories, service and repository exceptions, response handling, and Better Auth flows. Keep new tests behavior-focused and run the narrowest relevant suite before the full server suite.
Use LOG_LEVEL=error node ace test for broad runs to reduce log noise. Use pnpm run test:adapter for the Vitest adapter suite rather than trying to run it through Ace.
See Also
- AdonisJS Commands —
node ace testflags - Database Models — model patterns used in factories
- Japa documentation — official runner reference