Testing
Testing is crucial for maintaining reliable applications. This guide covers testing strategies for Adonis ODM, including unit tests, integration tests, and testing best practices for MongoDB operations.
Test Setup
Basic Test Configuration
// tests/bootstrap.ts
import { configure } from '@japa/runner'
import { assert } from '@japa/assert'
import { expectTypeOf } from '@japa/expect-type'
import { fileSystem } from '@japa/file-system'
import { apiClient } from '@japa/api-client'
configure({
plugins: [assert(), expectTypeOf(), fileSystem(), apiClient()],
setup: [
() => import('../test-helpers/setup.js'),
],
teardown: [
() => import('../test-helpers/teardown.js'),
],
})
Database Setup for Tests
// test-helpers/setup.ts
import Database from '@ioc:Adonis/Lucid/Database'
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default async function setupTests(app: ApplicationContract) {
// Use test database
const testConnection = {
client: 'mongodb',
connection: {
host: process.env.MONGO_TEST_HOST || 'localhost',
port: process.env.MONGO_TEST_PORT || 27017,
database: process.env.MONGO_TEST_DATABASE || 'test_db',
username: process.env.MONGO_TEST_USERNAME,
password: process.env.MONGO_TEST_PASSWORD,
}
}
// Override default connection for tests
Database.manager.add('test', testConnection)
Database.manager.connect('test')
}
Test Cleanup
// test-helpers/teardown.ts
import Database from '@ioc:Adonis/Lucid/Database'
export default async function teardownTests() {
// Clean up test database
await Database.manager.closeAll()
}
Model Testing
Basic Model Tests
import { test } from '@japa/runner'
import User from '#models/user'
import { DateTime } from 'luxon'
test.group('User Model', (group) => {
group.each.setup(async () => {
// Clean database before each test
await User.query().delete()
})
test('should create a user', async ({ assert }) => {
const userData = {
name: 'John Doe',
email: '[email protected]',
age: 30
}
const user = await User.create(userData)
assert.exists(user._id)
assert.equal(user.name, userData.name)
assert.equal(user.email, userData.email)
assert.equal(user.age, userData.age)
assert.isTrue(user.$isPersisted)
})
test('should find user by email', async ({ assert }) => {
const user = await User.create({
name: 'Jane Doe',
email: '[email protected]',
age: 25
})
const foundUser = await User.findBy('email', '[email protected]')
assert.exists(foundUser)
assert.equal(foundUser!._id, user._id)
assert.equal(foundUser!.name, user.name)
})
test('should update user', async ({ assert }) => {
const user = await User.create({
name: 'Bob Smith',
email: '[email protected]',
age: 35
})
user.name = 'Robert Smith'
user.age = 36
await user.save()
const updatedUser = await User.findOrFail(user._id)
assert.equal(updatedUser.name, 'Robert Smith')
assert.equal(updatedUser.age, 36)
})
test('should delete user', async ({ assert }) => {
const user = await User.create({
name: 'Alice Johnson',
email: '[email protected]',
age: 28
})
await user.delete()
const deletedUser = await User.find(user._id)
assert.isNull(deletedUser)
})
})
Validation Testing
import { test } from '@japa/runner'
import { ValidationException } from 'adonis-odm'
import User from '#models/user'
test.group('User Validation', (group) => {
group.each.setup(async () => {
await User.query().delete()
})
test('should validate required fields', async ({ assert }) => {
await assert.rejects(
async () => {
await User.create({
// Missing required name field
email: '[email protected]'
})
},
ValidationException
)
})
test('should validate email format', async ({ assert }) => {
await assert.rejects(
async () => {
await User.create({
name: 'Test User',
email: 'invalid-email'
})
},
ValidationException
)
})
test('should validate unique email', async ({ assert }) => {
await User.create({
name: 'First User',
email: '[email protected]'
})
await assert.rejects(
async () => {
await User.create({
name: 'Second User',
email: '[email protected]' // Duplicate email
})
}
)
})
test('should validate age range', async ({ assert }) => {
await assert.rejects(
async () => {
await User.create({
name: 'Young User',
email: '[email protected]',
age: 12 // Below minimum age
})
},
ValidationException
)
})
})
Query Testing
Query Builder Tests
import { test } from '@japa/runner'
import User from '#models/user'
test.group('User Queries', (group) => {
group.each.setup(async () => {
await User.query().delete()
// Create test data
await User.createMany([
{ name: 'John Doe', email: '[email protected]', age: 30, status: 'active' },
{ name: 'Jane Smith', email: '[email protected]', age: 25, status: 'active' },
{ name: 'Bob Johnson', email: '[email protected]', age: 35, status: 'inactive' },
{ name: 'Alice Brown', email: '[email protected]', age: 28, status: 'active' }
])
})
test('should filter by status', async ({ assert }) => {
const activeUsers = await User.query()
.where('status', 'active')
.all()
assert.equal(activeUsers.length, 3)
activeUsers.forEach(user => {
assert.equal(user.status, 'active')
})
})
test('should filter by age range', async ({ assert }) => {
const youngUsers = await User.query()
.where('age', '<', 30)
.all()
assert.equal(youngUsers.length, 2)
youngUsers.forEach(user => {
assert.isBelow(user.age, 30)
})
})
test('should order by age', async ({ assert }) => {
const users = await User.query()
.orderBy('age', 'asc')
.all()
assert.equal(users.length, 4)
assert.equal(users[0].age, 25)
assert.equal(users[1].age, 28)
assert.equal(users[2].age, 30)
assert.equal(users[3].age, 35)
})
test('should paginate results', async ({ assert }) => {
const result = await User.query()
.orderBy('name', 'asc')
.paginate(1, 2)
assert.equal(result.data.length, 2)
assert.equal(result.meta.total, 4)
assert.equal(result.meta.page, 1)
assert.equal(result.meta.perPage, 2)
assert.equal(result.meta.lastPage, 2)
})
test('should count records', async ({ assert }) => {
const totalCount = await User.query().count()
const activeCount = await User.query().where('status', 'active').count()
assert.equal(totalCount, 4)
assert.equal(activeCount, 3)
})
})
Aggregation Tests
import { test } from '@japa/runner'
import User from '#models/user'
test.group('User Aggregations', (group) => {
group.each.setup(async () => {
await User.query().delete()
await User.createMany([
{ name: 'John', department: 'IT', salary: 50000, age: 30 },
{ name: 'Jane', department: 'IT', salary: 60000, age: 25 },
{ name: 'Bob', department: 'HR', salary: 45000, age: 35 },
{ name: 'Alice', department: 'HR', salary: 55000, age: 28 }
])
})
test('should group by department', async ({ assert }) => {
const stats = await User.query()
.aggregate([
{
$group: {
_id: '$department',
count: { $sum: 1 },
avgSalary: { $avg: '$salary' },
avgAge: { $avg: '$age' }
}
},
{ $sort: { _id: 1 } }
])
assert.equal(stats.length, 2)
const itStats = stats.find(s => s._id === 'IT')
const hrStats = stats.find(s => s._id === 'HR')
assert.equal(itStats.count, 2)
assert.equal(itStats.avgSalary, 55000)
assert.equal(hrStats.count, 2)
assert.equal(hrStats.avgSalary, 50000)
})
})
Relationship Testing
Testing Model Relationships
import { test } from '@japa/runner'
import User from '#models/user'
import Post from '#models/post'
test.group('User-Post Relationships', (group) => {
group.each.setup(async () => {
await User.query().delete()
await Post.query().delete()
})
test('should create post with author', async ({ assert }) => {
const user = await User.create({
name: 'John Doe',
email: '[email protected]'
})
const post = await Post.create({
title: 'Test Post',
content: 'This is a test post',
authorId: user._id
})
assert.equal(post.authorId, user._id)
})
test('should preload author with posts', async ({ assert }) => {
const user = await User.create({
name: 'Jane Doe',
email: '[email protected]'
})
await Post.createMany([
{ title: 'Post 1', content: 'Content 1', authorId: user._id },
{ title: 'Post 2', content: 'Content 2', authorId: user._id }
])
const posts = await Post.query()
.preload('author')
.where('authorId', user._id)
.all()
assert.equal(posts.length, 2)
posts.forEach(post => {
assert.exists(post.author)
assert.equal(post.author.name, 'Jane Doe')
})
})
test('should query posts by author', async ({ assert }) => {
const user = await User.create({
name: 'Bob Smith',
email: '[email protected]'
})
await Post.createMany([
{ title: 'Bob Post 1', content: 'Content 1', authorId: user._id },
{ title: 'Bob Post 2', content: 'Content 2', authorId: user._id }
])
const userPosts = await user.related('posts').query().all()
assert.equal(userPosts.length, 2)
userPosts.forEach(post => {
assert.equal(post.authorId, user._id)
})
})
})
Transaction Testing
Testing Database Transactions
import { test } from '@japa/runner'
import Database from '@ioc:Adonis/Lucid/Database'
import User from '#models/user'
import UserProfile from '#models/user_profile'
test.group('Transactions', (group) => {
group.each.setup(async () => {
await User.query().delete()
await UserProfile.query().delete()
})
test('should commit transaction on success', async ({ assert }) => {
const result = await Database.transaction(async (trx) => {
const user = await User.create({
name: 'John Doe',
email: '[email protected]'
}, { client: trx })
const profile = await UserProfile.create({
userId: user._id,
bio: 'Software developer'
}, { client: trx })
return { user, profile }
})
// Verify data was committed
const user = await User.find(result.user._id)
const profile = await UserProfile.find(result.profile._id)
assert.exists(user)
assert.exists(profile)
assert.equal(profile!.userId, user!._id)
})
test('should rollback transaction on error', async ({ assert }) => {
await assert.rejects(async () => {
await Database.transaction(async (trx) => {
await User.create({
name: 'Jane Doe',
email: '[email protected]'
}, { client: trx })
// Simulate error
throw new Error('Transaction failed')
})
})
// Verify rollback - no user should exist
const userCount = await User.query().count()
assert.equal(userCount, 0)
})
test('should handle nested transactions', async ({ assert }) => {
const result = await Database.transaction(async (outerTrx) => {
const user = await User.create({
name: 'Alice Johnson',
email: '[email protected]'
}, { client: outerTrx })
await Database.transaction(async (innerTrx) => {
await UserProfile.create({
userId: user._id,
bio: 'Product manager'
}, { client: innerTrx })
}, { client: outerTrx })
return user
})
const user = await User.find(result._id)
const profile = await UserProfile.findBy('userId', result._id)
assert.exists(user)
assert.exists(profile)
})
})
Embedded Documents Testing
Testing Nested Data
import { test } from '@japa/runner'
import Order from '#models/order'
test.group('Order with Items', (group) => {
group.each.setup(async () => {
await Order.query().delete()
})
test('should create order with items', async ({ assert }) => {
const order = await Order.create({
customerId: 'customer123',
items: [
{ productId: 'prod1', name: 'Product 1', quantity: 2, price: 10.00 },
{ productId: 'prod2', name: 'Product 2', quantity: 1, price: 20.00 }
],
total: 40.00
})
assert.exists(order._id)
assert.equal(order.items.length, 2)
assert.equal(order.items[0].quantity, 2)
assert.equal(order.items[1].price, 20.00)
assert.equal(order.total, 40.00)
})
test('should query orders by item properties', async ({ assert }) => {
await Order.createMany([
{
customerId: 'customer1',
items: [{ productId: 'prod1', name: 'Laptop', quantity: 1, price: 1000 }],
total: 1000
},
{
customerId: 'customer2',
items: [{ productId: 'prod2', name: 'Mouse', quantity: 2, price: 25 }],
total: 50
}
])
const expensiveOrders = await Order.query()
.whereEmbedded('items', (query) => {
query.where('price', '>', 500)
})
.all()
assert.equal(expensiveOrders.length, 1)
assert.equal(expensiveOrders[0].customerId, 'customer1')
})
})
Performance Testing
Testing Query Performance
import { test } from '@japa/runner'
import User from '#models/user'
test.group('Performance Tests', (group) => {
group.timeout(30000) // 30 second timeout for performance tests
test('should handle large dataset efficiently', async ({ assert }) => {
// Create large dataset
const users = Array.from({ length: 1000 }, (_, i) => ({
name: `User ${i}`,
email: `user${i}@example.com`,
age: 20 + (i % 50)
}))
const startTime = Date.now()
await User.createMany(users)
const createTime = Date.now() - startTime
// Test query performance
const queryStartTime = Date.now()
const activeUsers = await User.query()
.where('age', '>', 30)
.limit(100)
.all()
const queryTime = Date.now() - queryStartTime
assert.isBelow(createTime, 5000) // Should create 1000 users in under 5 seconds
assert.isBelow(queryTime, 1000) // Should query in under 1 second
assert.isAtMost(activeUsers.length, 100)
})
test('should paginate efficiently', async ({ assert }) => {
// Create test data
const users = Array.from({ length: 500 }, (_, i) => ({
name: `User ${i}`,
email: `user${i}@example.com`,
age: 20 + (i % 50)
}))
await User.createMany(users)
const startTime = Date.now()
const result = await User.query()
.orderBy('name', 'asc')
.paginate(10, 20) // Page 10, 20 per page
const queryTime = Date.now() - startTime
assert.isBelow(queryTime, 500) // Should paginate in under 500ms
assert.equal(result.data.length, 20)
assert.equal(result.meta.page, 10)
})
})
Test Utilities
Custom Test Helpers
// test-helpers/model-factory.ts
import User from '#models/user'
import Post from '#models/post'
export class ModelFactory {
static async createUser(overrides: Partial<any> = {}) {
return await User.create({
name: 'Test User',
email: `test${Date.now()}@example.com`,
age: 25,
status: 'active',
...overrides
})
}
static async createPost(authorId?: string, overrides: Partial<any> = {}) {
const author = authorId ? { _id: authorId } : await this.createUser()
return await Post.create({
title: 'Test Post',
content: 'This is test content',
authorId: author._id,
isPublished: true,
...overrides
})
}
static async createUserWithPosts(postCount: number = 3) {
const user = await this.createUser()
const posts = []
for (let i = 0; i < postCount; i++) {
const post = await this.createPost(user._id, {
title: `Post ${i + 1}`,
content: `Content for post ${i + 1}`
})
posts.push(post)
}
return { user, posts }
}
}
Database Assertions
// test-helpers/database-assertions.ts
import { Assert } from '@japa/assert'
declare module '@japa/assert' {
interface Assert {
databaseHas(model: any, attributes: Record<string, any>): Promise<void>
databaseMissing(model: any, attributes: Record<string, any>): Promise<void>
databaseCount(model: any, expectedCount: number): Promise<void>
}
}
Assert.macro('databaseHas', async function (model, attributes) {
const record = await model.query().where(attributes).first()
this.exists(record, `Expected database to have record with ${JSON.stringify(attributes)}`)
})
Assert.macro('databaseMissing', async function (model, attributes) {
const record = await model.query().where(attributes).first()
this.isNull(record, `Expected database to not have record with ${JSON.stringify(attributes)}`)
})
Assert.macro('databaseCount', async function (model, expectedCount) {
const count = await model.query().count()
this.equal(count, expectedCount, `Expected ${expectedCount} records, got ${count}`)
})
Best Practices
1. Test Organization
// Good: Organize tests by feature/model
test.group('User Authentication', () => {
// Authentication-related tests
})
test.group('User Profile Management', () => {
// Profile-related tests
})
// Good: Use descriptive test names
test('should hash password before saving user', async ({ assert }) => {
// Test implementation
})
2. Test Data Management
// Good: Clean up after each test
group.each.setup(async () => {
await User.query().delete()
})
// Good: Use factories for consistent test data
const user = await ModelFactory.createUser({ age: 30 })
3. Assertion Patterns
// Good: Test both positive and negative cases
test('should validate email format', async ({ assert }) => {
// Test valid email
const user = await User.create({
name: 'Test',
email: '[email protected]'
})
assert.exists(user._id)
// Test invalid email
await assert.rejects(async () => {
await User.create({
name: 'Test',
email: 'invalid-email'
})
})
})
Next Steps
- Error Handling - Test error scenarios
- Performance Optimization - Performance testing strategies
- Model Lifecycle - Test lifecycle hooks
- Database Transactions - Transaction testing patterns