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.
| Tool | Purpose |
|---|---|
| Japa | Test runner and framework |
| @japa/assert | Assertion library |
| @japa/api-client | HTTP API testing |
| @adonisjs/auth/plugins/api_client | Authenticated requests |
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
Tests are organized by domain:
| Domain | Test Files | Coverage |
|---|---|---|
| Users | 15 | Registration, login, logout, email verification, password reset, magic links, profile updates |
| Teams | 16 | Create, update, delete, members, invitations, roles |
| Billing | 4 | Checkout, portal, status, invoices |
| Plans | 1 | List all plans |
| Invitations | 1 | Get invitation details |
| Webhooks | 1 | Stripe webhook handling |
The test bootstrap (tests/bootstrap.ts) configures:
Before all tests run:
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
setup: [
() => testUtils.db().truncate(),
() => testUtils.db().migrate(),
() => runTestSeeders()
],
teardown: [],
}
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 populate the database with known data:
| Seeder | Creates |
|---|---|
1_user_seeder.ts | Test users (jane.doe@example.com) |
2_team_seeder.ts | Test teams with members |
3_invitation_seeder.ts | Pending invitations |
4_plan_seeder.ts | Subscription plans |
Seeders run in order (by filename) before tests start.
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()
})
| Method | Purpose |
|---|---|
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 |
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())
})
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()
})
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
]
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)
})
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')
})
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 })
})
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
})