Skip to content

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:

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 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 UserFactory

Event 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 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()

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

Built with ❤️ by the Jubiloop team