testing
Testing guidelines. Use when writing or updating tests.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Testing guidelines. Use when writing or updating tests.
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Run, write, and troubleshoot Japa browser tests that use Playwright; use when you add or change frontend functionality that should be covered by tests
Writing documentation. Use when key functionality is added or changed
Caching with @adonisjs/cache (keys, tags, invalidation). Use when adding or debugging cache behavior.
Lucid models/migrations conventions and safe workflows. Use when changing schema or models.
Frontend rules and React Router conventions. Use when working with frontend-related tasks.
Background job conventions (BullMQ via @rlanz/bull-queue). Use when adding or editing jobs.
Basado en la clasificación ocupacional SOC
| name | testing |
| description | Testing guidelines. Use when writing or updating tests. |
Docs: https://docs.adonisjs.com/guides/testing/introduction
tests/ and are based on AdonisJS Japa librarynode ace make:test --suite=<functional|browser|unit>LOG_LEVEL=info node ace test functional|browser|unit
node ace test --groups=""createTestActors from tests/utils/test_actors.ts; do not reuse shared actorsawait app.container.make(ClassName)test.group('User', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
})
import { test } from '@japa/runner'
test.group('User', (group) => {
group.each.setup(() => testUtils.db().truncate())
})
Core Principle: Use Fakes via Dependency Injection (container.swap) for testing interactions between your internal application services.
Why Fakes?
When to Mock? Reserve mocking primarily for truly external systems (e.g., third-party HTTP APIs) where a fake implementation isn't practical.
app.container.swap(ServiceIdentifier, () => new FakeService())
Define a simple FakeService class. It can extend the original service or simply implement the necessary methods/properties used by the Service Under Test (SUT). Only override methods critical for the test scenario.
Since you define the FakeService, you control its implementation. Add properties or methods to your fake class to track calls:
this.lastArgs = args or this.calls.push(args))this.methodWasCalled = true)Accessing the Fake: Keep a reference to the instance of the FakeService you create before passing its factory function to swap. Use this reference in your assertions.
group.each.setup.app.container.restore(ServiceIdentifier) or app.container.restoreAll() in test.cleanup or group.each.teardown.// Scenario: A UserService calls NotificationService.sendWelcomeEmail(user) upon registration
// Create a fake NotificationService
class FakeNotificationService extends NotificationService {
public sendWelcomeEmailCalled = false
public welcomeEmailUser: User | null = null
async sendWelcomeEmail(user: User) {
this.sendWelcomeEmailCalled = true
this.welcomeEmailUser = user
// Don't actually send an email
}
// Other methods can be omitted or throw if unexpected calls occur
}
// Test using the fake
test('registration sends welcome email', async ({ assert }) => {
const fakeNotifier = new FakeNotificationService()
app.container.swap(NotificationService, () => fakeNotifier)
const userService = await app.container.make(UserService)
const newUser = await userService.register({ email: 'test@example.com' /* ... */ })
assert.isTrue(fakeNotifier.sendWelcomeEmailCalled)
assert.equal(fakeNotifier.welcomeEmailUser?.id, newUser.id)
assert.equal(fakeNotifier.welcomeEmailUser?.email, 'test@example.com')
// Remember to restore
app.container.restore(NotificationService)
})
Always wrap database-mutating tests in transactions:
test.group('My service tests', (group) => {
group.each.setup(async () => {
await db.beginGlobalTransaction()
return () => db.rollbackGlobalTransaction()
})
// tests...
})
If your Service Under Test directly interacts with the database, test the real service implementation. Rely on the DB transaction for cleanup and isolation.
test('user service stores user in DB', async ({ assert }) => {
const userService = await app.container.make(UserService)
const newUser = await userService.register({ username: 'testuser' })
// Assert user was properly stored
assert.isNotNull(newUser.id)
// Verify with a direct DB query
const storedUser = await User.findOrFail(newUser.id)
assert.equal(storedUser.username, 'testuser')
})
Fake the dependency service using container.swap. The fake should return predefined data to simulate the dependency's database operation. This isolates the SUT from any DB interactions of its dependencies.
test('quota service properly updates user credit balance', async ({ assert }) => {
// Fake the transaction service that normally writes to DB
class FakeTransactionService {
public lastTransaction = null
async createTransaction(data) {
this.lastTransaction = data
return { id: 'fake-tx-123', ...data }
}
}
const fakeTransactions = new FakeTransactionService()
app.container.swap('transactions', () => fakeTransactions)
// Service under test
const quotaService = await app.container.make(MonthlyQuotaService)
const user = testUser
const quota = await MonthlyCreditQuota.create({
userId: user.id,
maxSummaryCredits: 10000,
spentSummaryCredits: 4000,
nextUsageReset: DateTime.now().minus({ days: 1 }),
})
await quotaService.updateQuota({ quota: quota.id })
// Assert transaction was created with correct data
assert.equal(fakeTransactions.lastTransaction.userId, user.id)
assert.equal(fakeTransactions.lastTransaction.summaryCredits, 4000)
// Restore original service
app.container.restore('transactions')
})
fetch)Use this when your service makes HTTP requests to external, third-party APIs.
Use undici's MockAgent. It's the library powering Node.js's fetch and provides built-in mocking capabilities.
import { MockAgent, setGlobalDispatcher } from 'undici'
test('weather service fetches and processes data correctly', async ({ assert }) => {
// Setup mock agent
const agent = new MockAgent()
setGlobalDispatcher(agent)
// Setup intercept
agent
.get('https://api.weather.com')
.intercept({
path: '/forecast',
method: 'GET',
query: { city: 'oslo' },
})
.reply(200, {
temperature: 5,
condition: 'sunny',
})
// Test service
const weatherService = await app.container.make(WeatherService)
const forecast = await weatherService.getForecast('oslo')
// Assert
assert.equal(forecast.temperature, 5)
assert.equal(forecast.condition, 'sunny')
// Cleanup
agent.close()
})
AdonisJS provides official fakes for common modules:
Use these when testing interactions with those specific modules for convenience.
Use timekeeper for predictable control over Date.
import timekeeper from 'timekeeper'
test('expired tokens are rejected', async ({ assert }) => {
// Create token that expires in 10 minutes
const tokenService = await app.container.make(TokenService)
const token = await tokenService.create(testUser.id, { expiresInMinutes: 10 })
// Travel 30 minutes into the future
const futureTime = new Date()
futureTime.setMinutes(futureTime.getMinutes() + 30)
timekeeper.travel(futureTime)
// Assert token is now expired
assert.isFalse(await tokenService.verify(token))
// Reset time
timekeeper.reset()
})
To avoid boilerplate, create a test helper:
// in test_helpers.ts
import { getActiveTest } from '@japa/runner'
import timekeeper from 'timekeeper'
export function timeTravel(minutesToTravel: number) {
const test = getActiveTest()
if (!test) {
throw new Error('Cannot use "timeTravel" outside of a Japa test')
}
timekeeper.reset()
const date = new Date()
date.setMinutes(date.getMinutes() + minutesToTravel)
timekeeper.travel(date)
test.cleanup(() => {
timekeeper.reset()
})
}