Testing

Overview

Testing infrastructure with Japa.

Nuda Kit includes a comprehensive testing setup using Japa, the official testing framework for AdonisJS. The backend comes with 195+ pre-written tests covering authentication, teams, billing, and more.

Testing Stack

ToolPurpose
JapaTest runner and framework
@japa/assertAssertion library
@japa/api-clientHTTP API testing
@adonisjs/auth/plugins/api_clientAuthenticated requests

Test Structure

Tests are located in backend/tests/:

tests/
├── bootstrap.ts          # Test configuration
├── functional/           # API/integration tests
│   ├── billing/
│   │   ├── check_status.spec.ts
│   │   ├── create_checkout.spec.ts
│   │   ├── create_portal.spec.ts
│   │   └── get_invoices.spec.ts
│   ├── invitations/
│   │   └── get_invitation.spec.ts
│   ├── plans/
│   │   └── get_all_plans.spec.ts
│   ├── teams/
│   │   ├── accept_team_invitation.spec.ts
│   │   ├── create_team.spec.ts
│   │   ├── delete_team.spec.ts
│   │   └── ... (16 test files)
│   ├── users/
│   │   ├── create_user.spec.ts
│   │   ├── login_user_with_email_password.spec.ts
│   │   ├── verify_email_user.spec.ts
│   │   └── ... (15 test files)
│   └── webhooks/
│       └── stripe.spec.ts
└── helpers/
    ├── queue_fake.ts     # Queue mocking
    └── queue_test_utils.ts

Test Coverage

Tests are organized by domain:

DomainTest FilesCoverage
Users15Registration, login, logout, email verification, password reset, magic links, profile updates
Teams16Create, update, delete, members, invitations, roles
Billing4Checkout, portal, status, invoices
Plans1List all plans
Invitations1Get invitation details
Webhooks1Stripe webhook handling

Test Lifecycle

The test bootstrap (tests/bootstrap.ts) configures:

Setup Hooks

Before all tests run:

  1. Truncate database — Clear all tables
  2. Run migrations — Apply database schema
  3. Run test seeders — Seed test data
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
  setup: [
    () => testUtils.db().truncate(),
    () => testUtils.db().migrate(),
    () => runTestSeeders()
  ],
  teardown: [],
}

Per-Test Transactions

Each test runs in a database transaction that's rolled back after:

test.group('Users/Create', (group) => {
  group.each.setup(() => testUtils.db().withGlobalTransaction())
  
  // Tests run here...
  // Transaction is rolled back after each test
})

This ensures tests are isolated and don't affect each other.


Test Seeders

Test seeders populate the database with known data:

SeederCreates
1_user_seeder.tsTest users (jane.doe@example.com)
2_team_seeder.tsTest teams with members
3_invitation_seeder.tsPending invitations
4_plan_seeder.tsSubscription plans

Seeders run in order (by filename) before tests start.


Test Helpers

Queue Fake

Mock the queue to test job dispatching without processing:

import queueFake from '#tests/helpers/queue_fake'
import SendVerificationEmailJob from '#jobs/send_verification_email_job'

test('dispatches verification email', async ({ assert }) => {
  await queueFake.fake()

  // ... trigger action that dispatches job ...

  queueFake.assertDispatched(SendVerificationEmailJob)
  queueFake.assertDispatchedWith(SendVerificationEmailJob, { userId: 1 })

  await queueFake.restore()
})

Queue Fake Methods

MethodPurpose
fake()Start intercepting queue dispatches
restore()Restore original queue behavior
assertDispatched(Job, times?)Assert job was dispatched N times
assertDispatchedWith(Job, payload)Assert job with specific payload
assertNotDispatched(Job)Assert job was not dispatched
getDispatchedJobs()Get all dispatched jobs
clear()Clear dispatched jobs list

Event Fake

Mock events to test event emission:

import emitter from '@adonisjs/core/services/emitter'
import UserCreated from '#events/user_created'

test('emits user created event', async ({ cleanup }) => {
  const events = emitter.fake()

  // ... create user ...

  events.assertEmitted(UserCreated, ({ data }) => {
    return data.user.email === 'test@example.com'
  })

  cleanup(() => emitter.restore())
})

Mail Fake

Mock mail to test email sending:

import mail from '@adonisjs/mail/services/main'

test('sends welcome email', async ({ assert }) => {
  const { mails } = mail.fake()

  // ... trigger email ...

  assert.lengthOf(mails.sent, 1)
  assert.equal(mails.sent[0].subject, 'Welcome!')

  mail.restore()
})

Japa Plugins

The test setup includes these Japa plugins:

export const plugins: Config['plugins'] = [
  assert(),           // Assertions (assert.equal, assert.isTrue, etc.)
  apiClient(),        // HTTP client (client.get, client.post, etc.)
  pluginAdonisJS(app), // AdonisJS integration
  authApiClient(app), // Authenticated requests
]

Key Testing Patterns

Testing API Endpoints

test('creates a new user', async ({ client, assert }) => {
  const response = await client.post('/users').json({
    firstName: 'John',
    lastName: 'Doe',
    email: 'john@example.com',
    password: 'Password123!',
    password_confirmation: 'Password123!',
  })

  response.assertStatus(201)
  response.assertBody({ message: 'user created' })

  const user = await User.findBy('email', 'john@example.com')
  assert.isNotNull(user)
})

Testing Validation Errors

test('fails with invalid email', async ({ client, assert }) => {
  const response = await client.post('/users').json({
    email: 'invalid-email',
    // ...
  })

  response.assertStatus(422)
  assert.equal(response.body()['errors'][0].rule, 'email')
  assert.equal(response.body()['errors'][0].field, 'email')
})

Testing Authenticated Routes

test('returns authenticated user', async ({ client }) => {
  const user = await User.findByOrFail('email', 'jane.doe@example.com')

  const response = await client
    .get('/users/me')
    .loginAs(user)  // Authenticate as user

  response.assertStatus(200)
  response.assertBodyContains({ email: user.email })
})

Testing Authorization

test('only team owner can delete team', async ({ client }) => {
  const member = await User.findByOrFail('email', 'member@example.com')
  const team = await Team.findOrFail(1)

  const response = await client
    .delete(`/teams/${team.id}`)
    .loginAs(member)

  response.assertStatus(403) // Forbidden
})

Resources