filipebraida/adonisjs-starter-kit/testing

Claude Code and agent skill by filipebraida/adonisjs-starter-kit

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Japa test patterns for an AdonisJS app. Functional specs hit real Postgres wrapped in per-test transactions; unit specs stub deps with sinon. Fakes are mandatory for anything that reaches out (mail, drive, emitter, transmit). Trigger on: "write test", "add spec", "cover with tests", "how do we test X", "mock", "fake", "stub".

Install

npx vskill@latest install filipebraida/adonisjs-starter-kit/testing

SKILL.md

Testing

Two suites: unit for action/service internals with dependencies stubbed; functional for HTTP endpoints hit against a real Postgres wrapped in a per-test transaction. Every spec matches the shape of the nearest existing spec — same imports, same setup/teardown order, same helpers. Anything that reaches out (mail, drive, emitter, transmit, third-party HTTP) must be faked; a global mail fake in tests/bootstrap.ts is a safety net so a forgotten setup doesn't send a real email.

Mandatory fakes by dependency

Every side-effect dependency needs an explicit fake so no test reaches real infra:

DependencyHow to fakeScope
Mailmail.fake()Global in tests/bootstrap.ts + per-test when asserting
Drivedrive.fake(env.get('DRIVE_DISK') ?? 'fs')Per-group setup + teardown
Emitteremitter.fake(['event:name']) → EventsBufferPer-test opt-in
Transmit (SSE)transport: null in config/transmit.ts (default)Global

emitter.fake takes an array of event names — a bare string is a TypeScript error:

ts
const events = emitter.fake(['user:registered']) // ✅
// emitter.fake('user:registered') // ❌ wrong type

Rules

  • Location: app/<mod>/tests/{unit,functional}/*.spec.ts.
  • Group setup: every group opens with group.each.setup(() => testUtils.db().wrapInGlobalTransaction()) when the code under test touches DB (nearly always).
  • RBAC helpers: functional specs involving roles/permissions call group.each.setup(() => ensureBaseRoles()); assign per user with withRole(user, ROLES.X) / withPermissions(user, [...]).
  • Emitter fake: to isolate an endpoint from its listeners, const fake = emitter.fake(['event:name']) in setup + emitter.restore() in teardown. Assert with fake.assertEmitted('event:name') / fake.assertNoneEmitted().
  • Mail fake: const mails = mail.fake() in the test, then mails.assertSent(Notification, (msg) => ...) or mails.assertNoneSent().
  • Drive fake: drive.fake(...) in group setup, drive.restore(...) in teardown. Assert with disk.assertExists(path), disk.assertMissing(path).
  • Sinon for stubbing services, drivers, and single functions in unit tests: sinon.stub<[Arg], Promise<Ret>>().resolves(...). Always group.each.teardown(() => sinon.restore()).
  • Factories over fixtures — use UserFactory.create() / UserFactory.merge({...}).create() instead of hand-building rows.
  • CSRF: POST/PUT/DELETE must include .withCsrfToken() — shield middleware is enforced.
  • Auth: use .loginAs(user) on the request builder — sets the session cookie for that user's guard.
  • JSON vs HTML: chain .accept('json') when asserting a JSON response body, .withInertia() for Inertia flows so the exception handler treats the request as UI.
  • DB assertions: db.assertHas('table', { column: value }), db.assertMissing('table', { ... }) — plugin from @adonisjs/lucid/plugins/db.

Repo refs

  • Functional spec (per-test transaction via client): app/users/tests/functional/users_endpoint.spec.ts.
  • Unit spec (sinon, action in isolation): app/users/tests/unit/create_user.spec.ts.
  • RBAC helper: tests/helpers/rbac.ts. Bootstrap fakes: tests/bootstrap.ts.

Doc refs

  • Japa runner — https://japa.dev/docs
  • AdonisJS testing intro — https://docs.adonisjs.com/guides/testing/introduction
  • Fake mail / drive — https://docs.adonisjs.com/guides/digging-deeper/mail#fakes

Workflow

Functional endpoint spec
ts
import testUtils from '@adonisjs/core/services/test_utils'
import { test } from '@japa/runner'
import { UserFactory } from '#users/database/factories/user'
import { ROLES } from '#users/enums/role'
import { ensureBaseRoles, withRole } from '#tests/helpers/rbac'
test.group('Endpoint POST /invoices', (group) => {
group.each.setup(() => testUtils.db().wrapInGlobalTransaction())
group.each.setup(() => ensureBaseRoles())
test('admin creates invoice', async ({ client, db }) => {
const admin = await UserFactory.create()
await withRole(admin, ROLES.ADMIN)
const response = await client
.post('/invoices')
.loginAs(admin)
.withCsrfToken()
.json({ amount: 100, currency: 'BRL' })
response.assertStatus(302)
await db.assertHas('invoices', { amount: 100 })
})
})

Cover: unauth (302 → /login), forbidden (via .withInertia() + db.assertHas — see below), validation error (422 when JSON), happy path.

Testing authorization in an Inertia app

The exception handler for E_AUTHORIZATION_FAILURE branches on Accept:

  • JSON: returns 403.
  • HTML / Inertia (what the real client sends): flashes + redirect().back() → 302.

.assertStatus(403) on .accept('json') exercises a code path the UI never triggers. Assert the effect — the state that would change if the block failed:

ts
test('non-owner cannot delete another user\'s post', async ({ client, db }) => {
const owner = await UserFactory.create()
const stranger = await UserFactory.create()
const post = await PostFactory.merge({ userId: owner.id }).create()
await client
.delete(`/posts/${post.id}`)
.loginAs(stranger)
.withInertia()
.withCsrfToken()
.redirects(0)
await db.assertHas('posts', { id: post.id }) // row still exists → policy blocked
})

Rules of thumb:

  • Ownership tests use .withInertia() + db.assertMissing / db.assertHas to check the effect.
  • Only assert status codes when the client cares about the exact number (422, Location: /login).
Unit spec for an action
ts
import testUtils from '@adonisjs/core/services/test_utils'
import { test } from '@japa/runner'
import sinon from 'sinon'
import CreateInvoice from '#invoices/actions/create_invoice'
test.group('CreateInvoice', (group) => {
group.each.setup(() => testUtils.db().wrapInGlobalTransaction())
group.each.teardown(() => sinon.restore())
test('persists + emits invoice:issued', async ({ db, assert }) => {
const invoice = await new CreateInvoice().handle({
amount: 100,
currency: 'BRL',
})
assert.equal(invoice.amount, 100)
await db.assertHas('invoices', { id: invoice.id })
})
})

To stub an injected dep (like an auth guard), fake the ctx shape with as unknown as:

ts
const login = sinon.stub<[User], Promise<void>>().resolves()
const auth = { use: () => ({ login }) } as unknown as Parameters<Action['handle']>[0]['auth']
Faking side effects
  • Mail — asserted per-message:

``ts const mails = mail.fake() await someAction() mails.assertSent(WelcomeNotification, (msg) => msg.to === user.email) ``

  • Emitter — verify emit without running the listener:

``ts const fake = emitter.fake(['user:registered']) // ... trigger endpoint fake.assertEmitted('user:registered') ``

  • Emitter with listener — for end-to-end listener specs, do not fake the emitter. mail.fake() first (so the listener's mail send is captured, not sent), then await emitter.emit('user:registered', payload), then assert the effect (DB row created, mail sent, etc).

Anti-patterns

  • ❌ Skipping wrapInGlobalTransaction() — rows leak between tests, ordering matters, flakiness appears.
  • ❌ Hand-built fixture rows when a factory exists — inconsistent baseline data.
  • ❌ Real network / mail / drive calls in tests — flaky, slow, sometimes sends real emails.
  • ❌ POST/PUT/DELETE without .withCsrfToken() — will 403.
  • ❌ Asserting redirect location on a JSON call — chain .accept('json') instead.
  • ❌ Asserting the exception handler's 302 as if it were the policy's return — assert the resource state (assertHas / assertMissing), not the redirect.
  • ❌ Sharing state between tests via module-level variables — transaction rollback doesn't clean JS state.
  • ❌ Testing controllers directly (instantiate + call handler) — go through client so middleware, validators and policies actually run.
  • ❌ Faking the emitter and expecting the listener to run — the fake swallows the event.

Related skills

[[actions-events]] · [[authorization]] · [[crud]]

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
94
Repository forks
12
7-day trend score
23

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/filipebraida/adonisjs-starter-kit/testing/badge)](https://verified-skill.com/skills/filipebraida/adonisjs-starter-kit/testing)

Something wrong with this skill or its page? Tell us.