Skip to content

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:

SuitePatternTimeoutWhen to use
unittests/unit/**/*.spec.ts2 sPure logic, no HTTP, no DB
functionaltests/functional/**/*.spec.ts30 sFull 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 --grep flags. Use --files to 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)]
PluginWhat it provides
@japa/assertassert object in test context — assert.equal(), etc.
@japa/api-clientclient object — makes HTTP requests to the running server
@japa/plugin-adonisjsIntegrates 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 UserFactory

Event 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 EventFactory

Available Factories

All factories are in apps/server/tests/factories/:

FileModel
user.tsUser
organization.tsOrganization
member.tsMember
event.tsEvent
event_plan.tsEventPlan
event_block.tsEventBlock
event_task.tsEventTask
event_task_assignee.tsEventTaskAssignee
event_budget_item.tsEventBudgetItem
event_comment.tsEventComment
event_collaborator.tsEventCollaborator
event_personnel.tsEventPersonnel
event_permission.tsEventPermission

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

Built with ❤️ by the Jubiloop team