# Unit Testing

This guide explains the most important Vona testing workflows in the Cabloy monorepo.

## Why testing is emphasized

Keep testing proportional to the backend change: cover critical business behavior using the existing Vona testing infrastructure, and add focused regression coverage when actual problems are fixed. Choose the smallest meaningful service/model, controller/API, or integration boundary for the changed contract; browser E2E is not the default backend test layer.

Test-driven development and targeted unit or framework tests remain useful when they clarify and directly prove new or changed behavior. Exhaustive upfront mock/fake matrices and bespoke harnesses are not a delivery prerequisite.

Preserve applicable authentication/authorization, tenant-isolation, transaction, concurrency, idempotency, migration, and acceptance checks without turning them into an unrelated mandatory matrix. The context, fixture, and resource-isolation rules below still apply. Verify changes to migration, initialization, or seed behavior through managed isolated tests covering the affected behavior, preferably focused; a `meta.version.ts` edit alone does not require the full suite. Missing runtime access must be reported, not worked around with unauthorized resets or substitute test machinery.

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:

```bash
npm run vona :create:test student -- --module=training-student
```

## Execute tests

From the repository root, prefer a focused run for a bounded module change:

```bash
npm run test -- 'src/module/<module-name>/test/**/*.ts'
```

Resolve the actual test path for suite-owned modules. Positional patterns select test files, not a reduced database-preparation lifecycle. Focused managed tests still exercise fresh migration, initialization, and seeds; assert the affected resource behavior and cover existing-data upgrades when applicable. Expand to the full suite according to actual impact:

```bash
npm run test
```

A typical Vona test flow includes:

1. create one shared global `app` object for the test run
2. clean the run's Redis namespace and run-specific public/runtime paths
3. recreate the run's disposable test database
4. execute migration and instance initialization code, then module `seed()` hooks
5. run the test files
6. close the shared application after the test run

In Cabloy Basic and Cabloy Start, root `npm run test` delegates to `vona/`'s `npm run test`; both allocate an isolated test identity, database and Redis namespace, Vona generated/runtime/public paths, and loopback listener. They can run in the same checkout as an ordinary development server without resetting or mutating that server's business data. Even a targeted run performs the test-mode setup. Passing tests do not update the development database; choose its migration or explicitly approved reset path separately using [Migration and Changes](/backend/migration-and-changes#local-development-workflow). The Vona `npm run cov` script uses the same isolation; raw `:bin:test` commands do not. Shared build/dependency preparation may still write to shared artifacts, and custom external resources need separate isolation. See [Repo Scripts](/reference/repo-scripts#test-isolation-and-development-data).

The runner owns this application lifecycle. Test files import the shared app from `vona-mock` and must not create or close it themselves:

```typescript
import { app } from 'vona-mock';
```

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 development database without running tests

From the repository root:

```bash
npm run db:reset
```

This is an explicit, destructive **dev-mode** command, not test isolation: it recreates matching disposable development databases, reapplies migration and instance initialization, and runs module `seed()` hooks. It requires `database.testDatabase: true` in dev configuration and a connection outside the disposable database. Ordinary dev startup does not seed; do not reset if you need to keep its existing business data. See [Migration and Changes](/backend/migration-and-changes#local-development-workflow).

## Coverage

Representative command:

```bash
cd vona && npm run cov
```

This script runs under the same isolated test environment as Vona's `npm run test`. It writes `vona/coverage/lcov.info` and, unless `--ci` is supplied, `vona/coverage/report`. These reports are shared checkout-level artifacts rather than per-run test data; do not run coverage jobs simultaneously in one checkout. Use `npm run cov -- --ci` to skip the viewer in CI.

## Runner concurrency

The Vona runner uses Node's built-in test runner with one shared application and process isolation disabled. `TEST_CONCURRENCY` controls runner scheduling:

- `true` uses the available CPU count;
- `false` forces serial execution;
- a positive integer sets an explicit concurrency limit.

```bash
TEST_CONCURRENCY=false npm run test
TEST_CONCURRENCY=4 npm run test
```

Cabloy Basic requests concurrency by default with `TEST_CONCURRENCY=true`. When the active database dialect does not advertise the required concurrency capability, the runner falls back to serial execution. The default SQLite path therefore runs serially.

Runner concurrency schedules ordinary tests; it does not prove a business-level race condition. Tests must not rely on scheduling, timing, or execution order for correctness.

A suite can also make sibling-test scheduling explicit:

```typescript
describe('resource.test.ts', { concurrency: false }, () => {
  // sibling tests in this suite are serialized
});
```

This setting controls test scheduling only. It is not a substitute for explicitly creating competing business operations.

## Choose a test double through existing extension points

Before adding test-specific infrastructure, inspect how the dependency is created, configured, resolved, and retained. Prefer an existing supported configuration or dependency-replacement interface when it can express the scenario. Verify that the service, action, or HTTP request under test actually resolves the replacement through that normal path; crossing a request boundary alone does not require new middleware or a registration bridge.

Keep implementation selection separate from scenario state:

- **Configuration selects the implementation.** Use the existing test-mode configuration or supported replacement interface before application initialization or dependency creation, as required by its lifecycle. Do not assume an arbitrary config field can replace a dependency; confirm the owning implementation supports it. See [Config Guide](/backend/config-guide#project-config-is-layered-not-single-file) for configuration layering.
- **Fixtures own scenario state.** Keep controlled responses, fault switches, observations, and cleanup associated with exact test-owned resources. A configured shared implementation is not permission to share mutable scenario state or clear another test's records. Avoid temporarily mutating app-global configuration between concurrent tests.
- **Repository tests must be reproducible.** Check deterministic test setup into the repository; do not require private `config.test.local.ts` content for automated tests to work. Local overrides are suitable for deliberate local experiments, not an undocumented prerequisite for checked-in tests.
- **Keep the selected boundary honest.** Replacing one dependency does not replace authentication, active-instance scope, resource authorization, persistence, or lifecycle checks elsewhere in the tested path. Assert the behavior at the intended service, action, or HTTP boundary rather than treating the test double's call count as the whole outcome.
- **Prevent unintended external effects.** For tests intended to run without external I/O, verify the effective replacement before invoking the affected operation and make unsupported operations fail explicitly rather than fall back to a real external implementation. Do not read private credentials or contact an external service to compensate for missing setup.

Request-scoped replacement remains appropriate when the contract genuinely needs different implementations per request and ordinary configuration cannot express that requirement. Name that requirement first, then use an existing request-scoped extension point where available; introduce new transport or registration machinery only when necessary, with explicit ownership and cleanup. Do not let untrusted request input select a privileged test implementation.

### Configuration selection versus runtime decisions

Prefer existing configuration for static composition, such as candidate lists and implementation selection. Before adding or retaining an availability callback, check whether it merely repeats choices the supported configuration can already express. Remove that duplication rather than inventing another boolean or moving the same hardcoded predicate into a configuration callback. This is configuration-first, not a requirement to make every business condition configurable.

Keep these responsibilities distinct:

- **Configuration owns static composition.** Use the existing supported options to declare the intended candidates and defaults. Verify precedence, merge behavior, and array handling in the owning implementation; an override may replace a list rather than append to it, requiring the complete intended list.
- **Runtime business logic owns contextual decisions.** Retain a callback when the decision genuinely depends on the current user, resource state, or request context. Candidate membership alone does not establish authorization or lifecycle eligibility.
- **Each implementation owns its required-parameter validation.** Selecting an implementation does not prove that it is ready to execute. Test fixtures still own effective test-double checks, controlled scenario state, and cleanup; configuration is not proof of network isolation.
- **Removing a hardcoded restriction can change configuration authority.** State whether a later explicit override can now enable a previously prohibited choice. Verify the intended defaults and effective test composition, and distinguish those observations from untested configurations. Do not report configuration-driven selection as an immutable code-level prohibition.

## Mock request context

One of the most important Vona testing patterns is simulating a request context.

Representative shape:

```typescript
await app.bean.executor.mockCtx(async () => {
  // test logic here
});
```

Treat one `mockCtx(...)` callback as one simulated request boundary. Keep `app.ctx`, authentication, current database selection, and context-dependent service, model, or action work inside that callback, and always await it. Do not retain request-context state after the callback returns.

`mockCtx(...)` isolates request-local context. It does not isolate committed persisted records, app-global state, external caches, or shared durable fixtures. For independent concurrent requests, create one `mockCtx(...)` for each branch; parallel work started inside a single `mockCtx(...)` shares that request context.

Locale-sensitive variants and additional request-context helpers are also available.

## Persisted fixture lifecycle

Classify persisted test data before creating it:

- use a **test-local fixture** for a single test or scenario;
- use an owning module's `meta.version.ts` `seed()` hook only for stable baseline data shared across isolated test runs or intentionally prepared by an explicit dev-mode database reset.

A test owns every persisted resource it creates. Keep the returned entity or exact ID and delete owned records from `finally`, including when an assertion or action fails. Delete dependents before their owners and use the same active tenant/instance context that created them. Do not discover cleanup targets through broad table queries, time-based prefixes, or business conditions when the test already knows the exact identity.

```typescript
let parentId: number | undefined;
let childId: number | undefined;
try {
  parentId = await createParent();
  childId = await createChild(parentId);

  // exercise and assert the behavior under test
} finally {
  if (childId) await deleteChild(childId);
  if (parentId) await deleteParent(parentId);
}
```

Treat shared `seed()` records as read-only. If a scenario needs to change a record, create a separate test-local fixture instead and clean it up. See [Migration and Changes](/backend/migration-and-changes#seed-test-environment-baseline-data) for the durable seed lifecycle.

Application shutdown is runner-owned; authentication and fixture cleanup are test-owned. Do not call `app.close()` from a test. Keep `signout()` and exact-identity deletion in `finally` inside an appropriate request context.

## Testing concurrent behavior explicitly

A concurrency test creates competing business operations inside one test; it does not depend on the runner to happen to schedule tests at the same time.

```typescript
const attempt = async () => {
  return await app.bean.executor.mockCtx(async () => {
    await app.bean.passport.signinMock();
    try {
      return await reserve(resourceId);
    } finally {
      await app.bean.passport.signout();
    }
  });
};

const results = await Promise.allSettled([attempt(), attempt()]);
```

Use this sequence:

1. create a dedicated test-local fixture in an appropriate `mockCtx(...)`;
2. give every contender its own `mockCtx(...)` and authentication lifecycle;
3. launch the contenders explicitly and wait for all of them to settle;
4. use a fresh `mockCtx(...)` to assert both individual outcomes and the combined durable invariant;
5. clean up exact owned fixtures only after every branch has settled, in the appropriate tenant/instance context.

For example, a one-winner reservation test should verify not only the fulfilled and rejected operations but also the final balance, surviving reservation, and audit records. If the invariant depends on row locks, transaction isolation, or another database-specific capability, skip the test on unsupported dialects rather than weakening its assertions. See [stockReservation.test.ts](../../vona/src/suite/a-commerce/modules/commerce-trade/test/stockReservation.test.ts) for this pattern.

## Working with module scope in tests

Representative pattern:

```typescript
const scopeStudent = app.scope('training-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:

```typescript
await app.bean.executor.performAction('get', '/training/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:

```typescript
const updateRes = await app.bean.executor.performAction('patch', '/test/rest/product/:id', {
  params: { id: productId },
  body: dataUpdate,
});
assert.equal(updateRes, null);

const deleteRes = await app.bean.executor.performAction('delete', '/test/rest/product/:id', {
  params: { id: productId },
});
assert.equal(deleteRes, null);
```

For standard resource command mutations, these assertions verify the controller-facing no-payload runtime contract. Follow them with a query/read-back assertion to verify the persisted update or deletion. When the API is generated for frontend consumers, add a focused OpenAPI structural assertion that its wrapped response schema declares `data: null`; this verifies SDK-contract fidelity separately from responder behavior. The same test then exercises params, body, route wiring, validation, response behavior, and persistence effects 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`
- `catchError` from `@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:

1. create request data
2. sign in if auth is required
3. call create action
4. call list/query action and verify inclusion
5. call update action
6. call find-one action and verify the new state
7. call delete action
8. verify the final deleted/not-found state
9. 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:

- [CRUD Workflow](/backend/crud-workflow)
- [Migration and Changes](/backend/migration-and-changes)
- [Controller Guide](/backend/controller-guide)
- [Transaction Guide](/backend/transaction-guide)
- [Redlock Guide](/backend/redlock-guide)

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:

1. should a module test be created or updated?
2. does the change need request-context simulation?
3. does it affect migration/setup behavior that should be covered through the test flow?
4. should controller behavior be verified through `performAction` rather than only direct method calls?
5. 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.
