testing
Testing guidelines. Use when writing or updating tests.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Testing guidelines. Use when writing or updating tests.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Run, write, and troubleshoot Japa browser tests that use Playwright; use when you add or change frontend functionality that should be covered by tests
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.
Writing documentation. Use when key functionality is added or changed
Frontend rules conventions. Use when working with frontend-related tasks.
Background job conventions (BullMQ via @rlanz/bull-queue). Use when adding or editing jobs.
| name | testing |
| description | Testing guidelines. Use when writing or updating tests. |
| metadata | {"short-description":"Testing guidelines."} |
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=""tests/bootstrap.ts via testUtils.db().migrate()createTestActors from tests/utils/test_actors.ts; do not reuse shared actorsawait app.container.make(ClassName)testUtils.db().withGlobalTransaction():test.group('User', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
})
tests/bootstrap.ts, use suite-level wiring that adds test.setup(() => testUtils.db().withGlobalTransaction()) for each test in the suite.testUtils.db().truncate() instead so tables are cleared after each test.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)
})
Run migrations once globally, then wrap database-mutating tests in per-test transactions:
export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
setup: [() => testUtils.db().migrate()],
teardown: [],
}
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()
})
}