Custom agent imported from impresso/impresso-middle-layer (
.github/agents/feathers-service-tests.agent.md). Copyright stays with the author.
Keep the focus narrow.
- Start from the nearest existing test file and mirror its setup style.
- Use Mocha with
import { strict as assert } from 'assert'. - Mock the smallest app surface possible. Compose it from the
with*helpers inhelpers/app.tsviasetupTestApp(...), and includewithDatabase()only when the service actually needs Sequelize. Do not hand-roll a fakeapp. - Keep fixtures inline unless they are reused across files. Use small typed fixtures at the top of the test file (
mockUser1,mockCollectionForUser1, ...) with fixed dates. - Prefer direct assertions on returned values and thrown errors.
- Cover at least one success case and one failure case for the behavior being changed.
Building the mock app with setupTestApp
setupTestApp(...features)returns{ app, teardown, ...extras }.appexposes onlyget(key)andservice(name).app.get('x')returnsundefinedif nothing is registered.app.service('x')throwsUnexpected service request: x. If you hit that error, add the missing feature or handler instead of stubbing around it.- Only include the features the service under test actually touches:
| Service needs | Feature | Exposes |
|---|---|---|
| Sequelize models | withDatabase() (in-memory SQLite, timestamps: false) |
sequelize |
| Celery / Redis / magic link / auth config | withRedisCelery() |
celeryRunCalls, redisSetExCalls |
| Solr queries | withSolr() |
mockSolr, solrSelectCalls |
| BullMQ jobs | withQueueService() |
queueService, queueServiceCalls |
| Cache | withCacheManager() |
cacheManager |
Any other app.get('name') config |
withConfig('name', {...}) |
none |
Debug output from logger.debug |
withDebugLogging() (temporary, process-global) |
none |
- If none fits, add a new
withX()feature tohelpers/app.tsfollowing the same pattern: registerctx.getHandlers/ctx.serviceHandlers, push a cleanup intoctx.teardownFns, and return extras. Don't patchappinside a test file.
Test file skeleton
let testApp: ReturnType<typeof setupTestApp<[
ReturnType<typeof withDatabase>,
ReturnType<typeof withSolr>,
/* ...same order as the call below */
]>>
let service: MyService
before(async () => {
testApp = setupTestApp(withDatabase(), withSolr())
const someModel = SomeModel.sequelize(testApp.sequelize) // register models on testApp.sequelize
await testApp.sequelize.sync({ force: true })
service = new MyService(testApp.app)
})
after(async () => { await testApp.teardown() })
beforeEach(async () => {
await testApp.sequelize.truncate({ cascade: true })
// reset any mutable mocks here (see "Mocking and asserting")
})
- The type annotation and the
setupTestApp(...)call must list features in the same order. - Initialize models before
sync({ force: true }).
Mocking and asserting
- Solr: override
testApp.mockSolr.select = async () => ({...})inside the test to return the facets or docs you need. Assert on outgoing queries viatestApp.solrSelectCalls. - Queue: assert
testApp.queueServiceCallsas{ method, data }entries, in order. - Celery / Redis: assert
celeryRunCallsandredisSetExCalls. - State isolation:
testAppis created once inbefore, so overridden mocks and captured-call arrays persist across tests. InbeforeEach, reset them (e.g.testApp.solrSelectCalls.length = 0and restore the defaultmockSolr.select), or callsetupTestAppinbeforeEachif the suite is small. - Seed data with
model.create(...)/bulkCreate(...), casting mocksas anywhere models require it.
What to cover
The minimum is one success case and one failure case per changed behavior. Where relevant, also cover:
- Multiple users or entities, to prove isolation (user A never sees user B's data).
- Filtering and search parameters (
term, paginationlimit/offset). - The unauthenticated or empty case, e.g.
service.find({ query: {} } as any)returning{ data: [], pagination: { limit: 10, offset: 0, total: 0 } }. - Side effects (queue jobs, Celery tasks, Redis writes), asserted through the captured-call extras.
Style
- Prefer several focused
it(...)blocks over one long test. Eachitasserts one behavior. - No
console.login committed tests. UsewithDebugLogging()only temporarily, and run that file in isolation. - Remove unused imports.
- Import paths use the
@/alias and.jsextensions.
Ownership and permission logic
- Assert the exact error type and message.
- Verify unauthorized users cannot change fields they do not own.
- Verify the allowed path only changes the intended fields.
- Keep the test data small and readable.
Running tests
After editing tests, run the narrowest relevant test file first, in isolation:
npx mocha path/to/file.test.ts
Deliverable
Return the new or edited test file plus any additions to helpers/app.ts, with the narrowest relevant test run confirming it passes.
