Unit Testing
This guide explains the most important Vona testing workflows in the Cabloy monorepo.
Why testing is emphasized
Test-driven development remains a strong default in the Cabloy monorepo.
Vona’s testing story is valuable because it is closely integrated with:
- app initialization
- Redis cleanup
- database recreation
- migration execution
- request-context simulation
- action-level API verification
That means tests can exercise framework behavior in a realistic way.
Unit testing in the backend contract loop
A useful backend-contract-loop testing mental model is:
- migration prepares the structural state
- controllers expose the route/action surface
- services and models execute the business and persistence logic
- tests verify the resulting contract through scoped access and action execution
This is why Vona testing should not be reduced to isolated helper-unit testing only.
Create a test file
Example:
npm run vona :create:test student -- --module=demo-studentExecute tests
From the root repository:
npm run testA typical Vona test flow includes:
- create a global
appobject - clean Redis data
- recreate the database
- execute migration code
- run the test files
This is one of the most important distinctions from ordinary app flow: test execution rebuilds and verifies the framework lifecycle, not only the target function.
Reset database without running tests
Representative command:
cd vona && npm run db:resetThis is useful when you want to reapply migration logic without running the entire test suite.
Coverage
Representative command:
cd vona && npm run covMock request context
One of the most important Vona testing patterns is simulating a request context.
Representative shape:
await app.bean.executor.mockCtx(async () => {
// test logic here
});Locale-sensitive variants and additional request-context helpers are also available.
Working with module scope in tests
Representative pattern:
const scopeStudent = app.scope('demo-student');This lets tests exercise:
- services
- models
- entities
- controller actions
through the same scoped abstractions used in application code.
Testing controllers through actions
Representative pattern:
await app.bean.executor.performAction('get', '/demo/student');This is especially useful because it exercises the controller path more realistically than only unit-testing isolated helper functions.
A practical rule is:
- use direct service/model assertions when the test target is truly internal behavior
- use
performAction(...)when the goal is to verify the backend API contract as a controller-facing workflow
A representative contract-verification pattern is:
await app.bean.executor.performAction('patch', '/test/rest/product/:id', {
params: { id: productId },
body: dataUpdate,
});This is a good default because the same test can exercise params, body, route wiring, validation, and response behavior together.
Authentication simulation
Tests can also simulate signin and signout behavior.
Representative patterns include:
signinMock()signinMock('admin')signout()
This is important for testing permission-sensitive flows.
A practical CRUD-style pattern is:
- sign in
- call create via
performAction(..., { body }) - call list/query and verify inclusion
- call update via params + body
- call find-one and verify new state
- call delete and verify final state
- sign out
This keeps auth-sensitive CRUD verification close to the real controller contract path.
Assertion and error-handling helpers
Two practical testing helpers are:
- Node’s built-in
assert catchErrorfrom@cabloy/utils
These help keep tests explicit while still fitting the framework’s async execution style.
End-to-end CRUD test story
A realistic CRUD test usually verifies a whole backend thread, not only one method call.
A practical sequence is:
- create request data
- sign in if auth is required
- call create action
- call list/query action and verify inclusion
- call update action
- call find-one action and verify the new state
- call delete action
- verify the final deleted/not-found state
- sign out
This is the most framework-native verification path because it tests route, validation, DTO, service, model, and migration assumptions together.
Relationship to migration and CRUD generation
Read this guide together with:
A practical split is:
- CRUD generation creates the initial backend thread
- migration keeps that thread structurally valid over time
- tests verify the resulting contract through realistic execution
Implementation checks for backend testing changes
When adding or changing backend behavior, do not stop at code generation.
It should also ask:
- should a module test be created or updated?
- does the change need request-context simulation?
- does it affect migration/setup behavior that should be covered through the test flow?
- should controller behavior be verified through
performActionrather than only direct method calls? - does the change affect the end-to-end CRUD thread rather than only one isolated function?
That leads to much stronger and more framework-native verification.