Testing Strategy
Language: JavaScript
How tests are organized across modules. This document covers the layout (where tests live, how they're loaded, what runs them). For the rules of writing a single unit test see unit-test-authoring.md. For the testing tiers and CI/CD setup see module-testing.md.
On This Page
- Philosophy
- Test Directory Structure
- Test File Pattern
- Test
package.jsonPattern - Testing Levels
- Testing Adapter-Based Modules (Three-Tier Pattern)
- Testing Modules with Dependencies
- Global Test Runner Pattern
- Rules
- Module README as Context
Philosophy
- Every module must be independently testable
- Tests live inside each module in a
_test/directory - Tests must be runnable with
npm testfrom the module root - JavaScript modules use the Node.js built-in test runner (
node --test) - no external test framework required - Each test file is self-contained: load dependencies, set up data, run tests, report results
Test Directory Structure
Every module (helper, model, controller, core) follows this test layout:
[module-name]/
_test/
package.json # Test-specific dependencies (references parent module + helpers)
test.js # Main test file
mock-data/ # (Optional) Test fixtures and mock data
sample-input.json
sample-output.json
README.md # (Optional) Notes on test setup requirementsTest File Pattern
Standard pattern for modules with dependencies (Models, Core, Controllers):
// _test/test.js
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
// Load via simulating loader for proper dependency injection
const loadLib = require('./loader');
const Lib = loadLib();
const UserModel = Lib.UserModel;
describe('User Model', function () {
it('should create user with valid data', function () {
const user = UserModel.data.create({ name: 'John', email: 'john@example.com' });
assert.strictEqual(user.name, 'John');
});
});Pattern for pure modules without dependencies (Config, Errors):
// _test/test.js
'use strict';
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
// Direct require for pure modules
const CONFIG = require('../user.config');
describe('Config', function () {
it('should have valid status values', function () {
assert.ok(Array.isArray(CONFIG.STATUS_VALUES));
});
});Test package.json Pattern
The shape of _test/package.json (private flag, file:../ self-reference, registry semver for shared helpers, required scripts) is documented in publishing.md → Test Directory Structure. That is the canonical home; do not duplicate the example here.
Testing Levels
| Level | What | Where | Runner |
|---|---|---|---|
| Unit Tests | Individual functions in isolation | [module]/_test/test.js | node --test |
| Integration Tests | Module interactions (e.g., controller + model) | src/server/_test/ | node --test |
| End-to-End Tests | Full request flow through interface | src/server/interfaces/_test/ | node --test |
Testing Adapter-Based Modules (Three-Tier Pattern)
Modules that use the Adapter Pattern - a parent module plus N standalone backend packages - test in three tiers. Each tier runs at a different layer and answers a different question.
The Three Tiers
| Tier | Where it lives | Loads | Docker | What it answers |
|---|---|---|---|---|
| Tier 1 - Adapter unit | [adapter]/_test/test.js | The adapter only (no parent module) | Optional - only the adapter's backend | Does the adapter's translation layer work? Identifier quoting, type coercions, hash-mismatch behavior, batch chunking |
| Tier 2 - Parent logic | [parent]/_test/test.js | The parent module + an in-memory adapter fixture | None | Does the parent's pure logic work? Loader validation, policy decisions, cookie handling, JWT rotation |
| Tier 3 - Contract integration | [adapter]/_test/test.js | The parent module + the real adapter | Yes - the adapter's backend | Does this adapter satisfy the parent's contract end-to-end? Every public parent API path against a real backend |
The split lets each tier run at maximum speed: Tier 2 has zero Docker latency and exercises every code path of the parent; Tier 1 isolates adapter-specific concerns; Tier 3 catches the integration seams that the other two cannot.
In-Memory Fixture Pattern (Tier 2 Enabler)
The parent module's _test/ directory contains a createInMemory<Adapter>() helper that implements the full adapter contract using a plain in-process Map or array. This is what makes Tier 2 possible:
[module-root]/
_test/
memory-store.js createInMemory<Adapter>() - full contract, in-process
test.js Tier 2 tests - load parent with memory store
loader.js Test-time DI helperRequired properties of the in-memory fixture:
- Implements every method of the adapter contract, with the same return shapes as a real adapter (success/error envelopes,
recordvsrecords, etc.) - Holds state in module-private structures (Map, array). One fixture instance = one isolated dataset
- No external dependencies beyond Node built-ins
- Lives only in
_test/. Never published, never required from outside the test directory, never used in production code
The fixture is a structural duck-typed substitute for any real adapter. The parent's loader cannot tell it apart from the published packages - that is the whole point.
Shared Contract Suite Copy Pattern (Tier 3 Enabler)
When N adapters all implement the same contract, the integration tests for that contract are written once and run against each adapter. The suite lives in the parent module's _test/ directory and is copied (not exported, not deep-required) into each adapter's _test/ directory:
js-server-helper-[parent]/_test/store-contract-suite.js ← canonical source
js-server-helper-[parent]-store-sqlite/_test/store-contract-suite.js ← copy
js-server-helper-[parent]-store-postgres/_test/store-contract-suite.js ← copy
js-server-helper-[parent]-store-mysql/_test/store-contract-suite.js ← copy
js-server-helper-[parent]-store-mongodb/_test/store-contract-suite.js ← copyWhy copy instead of export and require:
| Concern | Outcome with copy | Outcome with export-and-require |
|---|---|---|
| Test code in published runtime package | Never - suite stays in _test/ | Test code leaks into runtime exports |
| Cross-package version coupling | None - each adapter has its own pinned snapshot | Adapter must follow parent's package version exactly |
npm install graph in _test/ | One file:../ for the adapter under test, registry pins for siblings | Deep require pulls test source from another package - fragile in CI |
| Audit which contract version an adapter was built against | Trivial - the file is right there | Requires inspecting parent's published source |
The suite is not exported through the parent module's package.json exports. It is a test-only artifact whose canonical home is the parent's _test/ directory; the per-adapter copies are working snapshots.
Reference Implementations
Two modules in the JS implementation repository implement this pattern end-to-end:
js-server-helper-auth(canonical). Parent ships an in-memory fixture (_test/memory-store.js) and a shared contract suite (_test/store-contract-suite.js). Each storage adapter copies the contract suite and runs it against the real backend in its own_test/test.js.js-server-helper-verify(same pattern, second example). Parent ships_test/memory-store.jsand_test/shared-store-suite.js. Each storage adapter copies and runs the suite against its backend.
The number of adapters per parent is not part of the contract and varies as new backends are added.
Note: The in-memory fixture for verify is currently defined inline in
_test/test.js. It must be extracted to_test/memory-store.jsbefore the adapter_test/directories can be created (they need to import it).
Testing Modules with Dependencies (Simulating Loader)
When a module (like a Model or Core module) depends on other modules via Lib, we use a simulating loader (_test/loader.js) to inject dependencies during testing. This mimics the production loader.js behavior in an isolated environment.
When to use the simulating loader:
- Tests that access cross-module dependencies (e.g., User tests that need
Lib.ContactModel) - Tests for modules that use
Lib.Utilsor other helpers - Tests for process functions that receive
Libinjection
When direct require() is sufficient:
- Tests for pure modules with no external dependencies (e.g., simple config or errors modules)
- Tests that only test the module's internal logic without calling other models/helpers
Simulating Loader Pattern:
// _test/loader.js
module.exports = function() {
const Lib = {};
// Load helpers
Lib.Utils = require('js-helper-utils');
// Load models with dependency injection
const UserConfig = require('../user/user.config');
Lib.UserModel = require('../user')(Lib, UserConfig);
// Inject Lib into process functions
Lib.UserProcess = Lib.UserModel.process(Lib);
return Lib;
};Test Usage:
// _test/test.js
const loadLib = require('./loader');
const Lib = loadLib();
describe('UserProcess', function() {
it('should format display name using Lib.Utils', function() {
const user = Lib.UserModel.data.create({ name: 'Shiv' });
const result = Lib.UserProcess.formatDisplayName(user);
assert.strictEqual(result, 'Shiv');
});
});Global Test Runner Pattern
For complex layers like Models, we use a single global test runner that executes entity-specific test suites. This ensures that all model-layer cross-references are tested together.
Location: src/model/_test/test.js
Execution: node --test test.js [sub-test-paths]
Rules
- Every exported function must have at least one test case.
- Tests must not depend on external services (DB, network) unless explicitly marked as integration tests.
- Mock external dependencies by injecting fake
Libobjects or using the simulating loader. - Test naming convention:
should [expected behavior] when [condition] - No test should modify files or have side effects outside its scope.
- Run all module tests before committing:
npm testfrom module root or using the global runner.
Further Reading
- Unit Test Authoring (JavaScript) - how to write a single unit test
- Module Testing - testing tiers, badges, and CI/CD
- Integration Testing - testing against real cloud services
- Module README Structure - what every README must contain (Universal Sections, class-specific sections, three-tier model with
ROBOTS.md) - Module Publishing -
_test/layout and thepackage.jsonshape it requires - Module Structure (JavaScript) - the factory pattern that test loaders mirror