This guide covers how to write tests in Nuda Kit following the existing patterns and conventions.
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
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
})
})
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
Use Domain/Action format:
test.group('Users/Create', (group) => { ... })
test.group('Teams/Update Member Role', (group) => { ... })
test.group('Billing/Create Checkout', (group) => { ... })
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', ...)
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:
Use the client object to make API requests:
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)
})
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)
})
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)
})
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)
})
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)
})
test('it fails if user is not logged in', async ({ client }) => {
const response = await client.get('/users/me')
response.assertStatus(401)
})
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
// Exact match
response.assertBody({
message: 'team created',
data: {
team: { id: 1, name: 'Test Team' }
}
})
// Partial match
response.assertBodyContains({
message: 'team created'
})
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)
})
| Method | Description |
|---|---|
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 |
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')
})
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)
})
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())
})
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 create known data before tests run.
| Role | Notes | |
|---|---|---|
jane.doe@example.com | Team Owner | Primary test user |
random.user@example.com | Team Member | Member of team 1 |
ted.mosby@example.com | — | Not in any team |
barney.stinson@nuda-kit.com | — | Available for tests |
deleted.user@example.com | — | Soft-deleted user |
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)
})
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.
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)
})
})