Testing

Writing Tests

How to write tests in Nuda Kit.

This guide covers how to write tests in Nuda Kit following the existing patterns and conventions.

Creating a Test File

Use the ace command to generate a new test file:

# Create a test in tests/functional/
node ace make:test users/create_user

# Create a test in a new domain folder
node ace make:test posts/create_post

This creates a test file with the basic structure:

tests/functional/users/create_user.spec.ts

Test File Structure

Create test files in tests/functional/ with the .spec.ts extension:

import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'
import User from '#models/user'

test.group('Domain/Action', (group) => {
  // Run each test in a transaction (rolled back after)
  group.each.setup(() => testUtils.db().withGlobalTransaction())

  test('it does something', async ({ client, assert }) => {
    // Test implementation
  })
})

Naming Conventions

File Names

Use descriptive names matching the action being tested:

tests/functional/
├── users/
│   ├── create_user.spec.ts
│   ├── login_user_with_email_password.spec.ts
│   └── update_user_password.spec.ts
├── teams/
│   ├── create_team.spec.ts
│   └── delete_team_member.spec.ts

Test Groups

Use Domain/Action format:

test.group('Users/Create', (group) => { ... })
test.group('Teams/Update Member Role', (group) => { ... })
test.group('Billing/Create Checkout', (group) => { ... })

Test Names

Start with it and describe the expected behavior:

test('it creates a new user', ...)
test('it fails with missing email validation', ...)
test('it fails if user is not logged in', ...)
test('it fails if user is not a team member', ...)

Database Transactions

Always wrap tests in transactions to ensure isolation:

test.group('Users/Create', (group) => {
  group.each.setup(() => testUtils.db().withGlobalTransaction())

  // Each test runs in its own transaction
  // Changes are rolled back after each test
})

This ensures:

  • Tests don't affect each other
  • Database stays clean between tests
  • Tests can run in parallel

Making HTTP Requests

Use the client object to make API requests:

GET Request

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

  const response = await client
    .get('/teams')
    .loginAs(user)

  response.assertStatus(200)
})

POST Request

test('it creates a new team', async ({ client }) => {
  const user = await User.findByOrFail('email', 'jane.doe@example.com')

  const response = await client
    .post('/teams')
    .json({
      name: 'Test Team',
    })
    .loginAs(user)

  response.assertStatus(201)
})

PUT/PATCH Request

test('it updates a member role', async ({ client }) => {
  const user = await User.findByOrFail('email', 'jane.doe@example.com')

  const response = await client
    .put('/teams/1/members/2/roles')
    .json({
      role: 'admin',
    })
    .loginAs(user)

  response.assertStatus(200)
})

DELETE Request

test('it deletes the team', async ({ client }) => {
  const user = await User.findByOrFail('email', 'jane.doe@example.com')

  const response = await client
    .delete('/teams/1')
    .loginAs(user)

  response.assertStatus(200)
})

Authentication

Authenticated Requests

Use loginAs() to authenticate:

test('it 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 this user

  response.assertStatus(200)
})

Testing Unauthenticated Access

test('it fails if user is not logged in', async ({ client }) => {
  const response = await client.get('/users/me')

  response.assertStatus(401)
})

Assertions

Response Status

response.assertStatus(200)  // OK
response.assertStatus(201)  // Created
response.assertStatus(400)  // Bad Request
response.assertStatus(401)  // Unauthorized
response.assertStatus(403)  // Forbidden
response.assertStatus(404)  // Not Found
response.assertStatus(422)  // Validation Error

Response Body

// Exact match
response.assertBody({
  message: 'team created',
  data: {
    team: { id: 1, name: 'Test Team' }
  }
})

// Partial match
response.assertBodyContains({
  message: 'team created'
})

Custom Assertions

test('it creates a user', async ({ client, assert }) => {
  const response = await client.post('/users').json({ ... })

  // Database assertions
  const user = await User.findBy('email', 'test@example.com')
  assert.isNotNull(user)
  assert.equal(user!.firstName, 'John')

  // Response assertions
  const body = response.body()
  assert.exists(body.data.token)
  assert.isString(body.data.token.token)
})

Common Assert Methods

MethodDescription
assert.equal(a, b)Strict equality
assert.isTrue(value)Value is true
assert.isFalse(value)Value is false
assert.isNull(value)Value is null
assert.isNotNull(value)Value is not null
assert.exists(value)Value is not undefined
assert.isString(value)Value is a string
assert.isArray(value)Value is an array
assert.lengthOf(arr, n)Array has n items
assert.instanceOf(obj, Class)Object is instance of Class

Testing Validation Errors

test('it fails with missing email validation', async ({ client, assert }) => {
  const response = await client.post('/users').json({
    firstName: 'John',
    lastName: 'Doe',
    // email is missing
    password: 'Password123!',
  })

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

test('it fails with invalid email format', 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 Authorization

test('it fails if user is not team owner', async ({ client }) => {
  const member = await User.findByOrFail('email', 'random.user@example.com')

  const response = await client
    .delete('/teams/1')
    .loginAs(member)  // Member, not owner

  response.assertStatus(403)
})

test('it fails if user is not a team member', async ({ client }) => {
  const outsider = await User.findByOrFail('email', 'ted.mosby@example.com')

  const response = await client
    .get('/teams/1')
    .loginAs(outsider)

  response.assertStatus(404)
})

Testing Events

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

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

  await client.post('/users').json({
    firstName: 'John',
    lastName: 'Doe',
    email: 'john@example.com',
    password: 'Password123!',
    password_confirmation: 'Password123!',
  })

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

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

Testing Jobs

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

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

  await client.post('/users').json({
    firstName: 'John',
    email: 'john@example.com',
    password: 'Password123!',
    password_confirmation: 'Password123!',
  })

  const jobs = queueFake.getDispatchedJobs()
  
  assert.lengthOf(jobs, 1)
  queueFake.assertDispatched(SendVerificationEmailJob)

  await queueFake.restore()
})

Test Seeders

Test seeders create known data before tests run.

Available Test Users

EmailRoleNotes
jane.doe@example.comTeam OwnerPrimary test user
random.user@example.comTeam MemberMember of team 1
ted.mosby@example.comNot in any team
barney.stinson@nuda-kit.comAvailable for tests
deleted.user@example.comSoft-deleted user

Using Seeded Data

test('it updates team', async ({ client }) => {
  // Use seeded user
  const user = await User.findByOrFail('email', 'jane.doe@example.com')
  
  // Use seeded team
  const team = await Team.findByOrFail('id', 1)

  const response = await client
    .put(`/teams/${team.id}`)
    .json({ name: 'Updated Name' })
    .loginAs(user)

  response.assertStatus(200)
})

Creating Test Data

When seeded data isn't enough, create data within the test:

test('it handles multiple teams', async ({ client, assert }) => {
  const user = await User.findByOrFail('email', 'jane.doe@example.com')

  // Create additional test data
  const team = await Team.create({ name: 'New Team' })
  await user.related('teams').attach({
    [team.id]: { role: 'owner', default: false }
  })

  const response = await client
    .get('/teams')
    .loginAs(user)

  assert.isTrue(response.body().data.teams.length >= 2)
})

Data created within tests is automatically rolled back due to the transaction wrapper.


Complete Test Example

import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'
import User from '#models/user'
import Team from '#models/team'
import TeamMember, { TeamMemberRole } from '#models/team_member'

test.group('Teams/Create', (group) => {
  group.each.setup(() => testUtils.db().withGlobalTransaction())

  test('it creates a new team', async ({ client, assert }) => {
    const user = await User.findByOrFail('email', 'jane.doe@example.com')

    const response = await client
      .post('/teams')
      .json({ name: 'Test Team' })
      .loginAs(user)

    // Verify database state
    const team = await Team.findBy('name', 'Test Team')
    assert.isNotNull(team)

    const teamMember = await TeamMember.query()
      .where('team_id', team!.id)
      .where('user_id', user.id)
      .first()

    assert.isNotNull(teamMember)
    assert.equal(teamMember!.role, TeamMemberRole.OWNER)

    // Verify response
    response.assertStatus(201)
    response.assertBodyContains({
      message: 'team created',
      data: {
        team: { name: 'Test Team' }
      }
    })
  })

  test('it fails with missing name validation', async ({ client, assert }) => {
    const user = await User.findByOrFail('email', 'jane.doe@example.com')

    const response = await client
      .post('/teams')
      .json({ name: '' })
      .loginAs(user)

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

  test('it fails if user is not logged in', async ({ client }) => {
    const response = await client.post('/teams')

    response.assertStatus(401)
  })
})

Resources