@fluojs/testing
v3.0.1
Published
Testing module construction and provider override utilities for Fluo applications.
Maintainers
Readme
@fluojs/testing
Node.js >=24.0.0 <27 request-level testing helpers, testing module construction, and provider overrides for fluo applications.
Preparing for the coordinated Node 24 release? Follow the consumer migration guide before upgrading packages.
Table of Contents
- Installation
- When to Use
- Quick Start
- Common Patterns
- Canonical TDD Ladder
- React Consumer Testing Recipe
- Public API
- Related Packages
- Example Sources
Installation
pnpm add -D @fluojs/testing vitestVitest ^4.1.11 is a required peer dependency for the mock helpers and the @fluojs/testing/vitest entrypoint. @babel/core is declared as a peer because the Vitest decorators plugin loads Babel from the consuming workspace; package managers may surface that peer even when you only use the non-Vitest harness subpaths.
If you use @fluojs/testing/vitest, install @babel/core in the consuming workspace as well because fluoBabelDecoratorsPlugin() invokes Babel at runtime. The Vitest plugin runs with enforce: 'pre' so decorator-bearing TypeScript reaches Babel before Vite 8 normal-stage Rolldown/Oxc transforms. It transforms .ts, .tsx, .mts, and .cts source ids after removing Vite query/hash suffixes, skips node_modules, and resolves the nearest root Babel config named babel.config.cjs, babel.config.mjs, babel.config.js, or babel.config.json:
pnpm add -D @babel/coreWhen to Use
- when you want to compile a real module graph but replace a few explicit providers with fakes
- when route-level tests should run through fluo's real dispatch stack without starting a network server
- when library or adapter packages need conformance and portability harnesses from responsibility-specific subpaths
- when starter templates need a stable baseline for unit, integration, and e2e-style tests
Quick Start
import { createTestApp } from '@fluojs/testing';
const app = await createTestApp({ rootModule: AppModule });
try {
const response = await app
.request('POST', '/users/')
.header('x-request-id', 'test-request-1')
.query('include', 'profile')
.principal({ subject: 'user-1', roles: ['admin'] })
.body({ name: 'Ada' })
.send();
expect(response.status).toBe(201);
} finally {
await app.close();
}Use createTestApp({ rootModule }) as the default HTTP/e2e-style path for application routes, guards, interceptors, DTO validation, request bodies, query parameters, headers, synthetic principals, request-scoped provider isolation, and serialized responses. Reach for createTestingModule(...) when the contract is module wiring, provider visibility, or provider/guard/interceptor overrides inside one slice.
Common Patterns
Override providers before compilation
import { createTestingModule } from '@fluojs/testing';
import { vi } from 'vitest';
const module = await createTestingModule({ rootModule: AppModule })
.overrideProvider(USER_REPOSITORY, {
create: vi.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
})
.compile();
let testError: unknown;
let testFailed = false;
let disposeError: unknown;
let disposeFailed = false;
try {
const service = await module.resolve(UserService);
} catch (error: unknown) {
testError = error;
testFailed = true;
} finally {
try {
await module.container.dispose();
} catch (error: unknown) {
disposeFailed = true;
disposeError = error;
}
}
if (testFailed) {
if (disposeFailed) {
throw new AggregateError(
[testError, disposeError],
'Test and testing module disposal both failed.',
);
}
throw testError;
}
if (disposeFailed) {
throw disposeError;
}The testing builder also supports overrideProviders([[token, value], ...]), overrideGuard(...), overrideInterceptor(...), and overrideFilter(...) for route-pipeline tests that need to replace cross-cutting behavior. Guard and interceptor overrides are request-path safe when the route references the same token via @UseGuards(...) or @UseInterceptors(...); filter overrides replace the token in the compiled module graph and should be paired with request-level coverage where that filter is registered in the runtime app surface. Retain every successfully compiled TestingModuleRef and dispose its caller-owned container from finally (or afterEach for suite setup) so passing, failing, and early-returning tests all release lifecycle resources. A completed container.dispose() is idempotent. Teardown failures surface; when an in-flight assertion can also fail, report both errors (for example with AggregateError) rather than suppressing or replacing the assertion failure.
compile() follows production module-bootstrap semantics for lifecycle-bearing singleton providers, including module-declared and overridden factory providers: it resolves the effective provider graph, runs onModuleInit() for each resolved instance, then runs onApplicationBootstrap() in the same provider order before the testing module is returned. The builder owns its internally created container until that return: if applying overrides, running lifecycle hooks, or synchronizing resolved singletons fails, it disposes the container before rejecting. Successful cleanup preserves the original compile failure; a cleanup failure is reported with the original failure in an AggregateError. Successful TestingModuleRef behavior is unchanged, and callers retain ownership of module.container.dispose() through an unconditional finally or afterEach cleanup. get() keeps DI ownership semantics for synchronous singleton and multi-provider paths, so repeated sync reads reuse the same singleton contributions and the container can still clean them up.
Preserve module identity with overrideModule()
createTestingModule({ rootModule }) requires an explicit root module so tests compile the same module graph shape that production bootstrap uses. When overrideModule(source, replacement) swaps imported modules, the compiled testing module preserves the original rootModule and compiled modules[].type identities while using the replacement imports for provider resolution. This keeps diagnostics, graph assertions, and module-introspection helpers tied to the application module classes you authored instead of synthetic test-only wrapper classes.
const module = await createTestingModule({ rootModule: AppModule })
.overrideModule(StripeModule, FakeStripeModule)
.compile();
expect(module.rootModule).toBe(AppModule);
expect(module.modules.some((compiledModule) => compiledModule.type === BillingModule)).toBe(true);Request-level tests with createTestApp()
import { createTestApp } from '@fluojs/testing';
const app = await createTestApp({ rootModule: AppModule });
try {
const response = await app
.request('POST', '/users/')
.header('authorization', 'Bearer test-token')
.query('include', ['profile', 'settings'])
.principal({ subject: 'user-1', roles: ['member'] })
.body({ name: 'Ada' })
.send();
expect(response.status).toBe(201);
} finally {
await app.close();
}app.request(...).send() is the preferred app-developer path because it keeps tests close to HTTP semantics without manual FrameworkRequest/FrameworkResponse stubs and creates the same isolated request-scoped DI boundary as runtime dispatch. Close the returned app from a finally block so assertion failures do not leak runtime resources. Keep app.dispatch(...), makeRequest(...), and raw FluoFactory.create(...) tests for adapter/runtime contracts, framework internals, or compatibility cases where the low-level dispatch boundary itself is what the test must prove.
For cookie-bound routes, use the object request overload with adapter-normalized cookie values:
const response = await app.request({
path: '/session',
cookies: { session: 'test-session' },
}).send();cookies is assigned directly to FrameworkRequest.cookies; it does not parse a Cookie header or introduce adapter-specific cookie semantics. TestingModuleRef.dispatch(...) accepts the same normalized cookie record.
createTestApp(...) accepts the same application bootstrap options as the runtime HTTP bootstrap, including providers, filters, converters, interceptors, middleware, observers, versioning, conditionalRequest, errorRepresentation, and diagnostics options. This lets application tests assert canonical JSON, negotiated HTML, conditional 304/412, HEAD, 406, and provider fallback behavior through the same virtual request pipeline. The testing helper prepends its request-context middleware while preserving caller-provided middleware in the same app middleware chain.
Mock helpers from explicit subpaths
import { createDeepMock, createMock } from '@fluojs/testing/mock';
import { vi } from 'vitest';
const repo = createMock<UserRepository>({ findById: vi.fn() });
const mailer = createDeepMock(MailService);asMock(fn) accepts only a function and narrows it to Vitest Mock<T>; it is not an arbitrary-value cast. mockToken(token, value) creates a ValueProvider descriptor shaped as { provide: token, useValue: value } for token-based overrides. createMock(..., { strict: true }) rejects access to unspecified members. DeepMocked<T> is exposed from the root @fluojs/testing package, @fluojs/testing/types, and @fluojs/testing/mock; all three paths intentionally share the same Vitest-compatible mock type boundary without importing Vitest peer declarations through non-mock runtime helpers. Consumers that do not use Vitest should import only non-mock helpers from @fluojs/testing/app, @fluojs/testing/module, or the harness subpaths.
Install vitest in the consuming workspace before using the mock helpers so the published runtime import resolves consistently.
Conformance and portability harnesses
Use subpaths like @fluojs/testing/platform-conformance, @fluojs/testing/platform-shell-lifecycle-conformance, @fluojs/testing/http-adapter-portability, and @fluojs/testing/web-runtime-adapter-portability when authoring framework-facing platform packages.
Use createPlatformShellLifecycleConformanceHarness({ createShell }) to verify every active start() / stop() overlap rejects with PlatformLifecycleConflictError, callback reentry remains conflict-safe before and after arbitrary awaits, and callers can retry after a failed transition settles. Keep component-level checks in createPlatformConformanceHarness(...).assertAll(); the PlatformShell lifecycle contract is intentionally a separate harness.
Portability harness cleanup is part of the contract: if setup, listen(), a run callback that surfaces a partial app, or an assertion fails after an app has been bootstrapped, the harness closes that partial app. If app.close() fails, the harness reports that cleanup failure, and when setup or an assertion already failed it raises an aggregate error that preserves both the original failure and the cleanup failure.
HttpAdapterPortabilityHarness and web-runtime portability harness methods are the public adapter contract checks. Prefer focused assertions such as assertSupportsCustomHttpRouteMethods(), assertPreservesMalformedCookieValues(), assertSupportsSseStreaming(), assertPreservesRawBodyForJsonAndText(), assertPreservesExactRawBodyBytesForByteSensitivePayloads(), assertExcludesRawBodyForMultipart(), assertDefaultsMultipartTotalLimitToMaxBodySize(), assertSettlesStreamDrainWaitOnClose(), assertReportsConfiguredHostInStartupLogs(), assertReportsHttpsStartupUrl(...), and assertRemovesShutdownSignalListenersAfterClose() instead of hand-rolled equivalents.
Use assertSupportsCustomHttpRouteMethods() to prove that the adapter executes body-bearing QUERY and representative PURGE routes through its real listener or fetch dispatch seam. The assertion keeps CONNECT outside ordinary routing conformance and does not require custom methods to use a native route handoff.
Use assertPreservesExactRawBodyBytesForByteSensitivePayloads() when an HTTP adapter must prove rawBody keeps byte-sensitive payload bytes intact across runtimes.
Use assertSupportsHttpErrorRepresentations() to prove JSON, HTML, HEAD, unsupported Accept
406, and already-committed response behavior. Network harnesses adapt the shared
NetworkHttpErrorRepresentationBootstrapOptions; fetch-style harnesses adapt
WebHttpErrorRepresentationBootstrapOptions. Supply createErrorRepresentationBootstrapOptions
when an adapter's bootstrap type contains additional required fields—the typed builder receives only
the common fixture fields and returns that adapter's complete bootstrap options without casts.
Use assertDoesNotCommitAbortedHttpErrorRepresentations() to start an HTML provider, abort through
the adapter's native request surface, and prove that neither the provider result nor canonical JSON
fallback is written after cancellation.
Canonical TDD Ladder
For application features, build tests from the smallest explicit dependency boundary outward:
- Unit: place
*.test.tsfiles next to the service, controller, helper, or failure branch undersrc/**. Construct the class directly with explicit fakes, or use@fluojs/testing/mockhelpers when typed mocks keep setup readable. - Slice/module integration: add
*.slice.test.tsfiles for DI wiring and provider override coverage withcreateTestingModule({ rootModule })orTest.createTestingModule({ rootModule }). - HTTP e2e-style: place app-level tests such as
test/app.e2e.test.tsaround the virtual request pipeline withcreateTestApp({ rootModule })andapp.request(...).send()as the default route assertion helper. Useapp.dispatch(...)only when a lower-level dispatch contract is the subject of the test. - Platform/conformance: use harness subpaths only for adapter/runtime package contracts, not ordinary application feature coverage.
src/users/
users.service.test.ts
users.controller.test.ts
users.slice.test.ts
test/
app.e2e.test.tsfluo differs from NestJS by requiring tests to name an explicit rootModule. The testing utilities compile the module graph you authored instead of inferring dependencies from legacy TypeScript design metadata or reflection flags.
React Consumer Testing Recipe
React applications keep the same testing ladder and add build/browser evidence at the existing boundaries instead of introducing a React-specific testing helper:
- Unit-test render-policy and metadata composition as pure values.
- Use
createTestApp({ rootModule })for direct page returns, missing-renderer diagnostics, DTO validation, request-scope identity, response ownership, guards, interceptors, and native mutation routes. Close the app infinally. - Run
fluo typegen ... --checkin CI and compile generated-route fixtures with TypeScript. Keep positive route-id/params cases and negative unknown-id, missing-param, extra-param, and stale-output cases. - Hydrate server markup with React DOM. Assert an aligned tree is interactive without diagnostics,
and capture
onRecoverableErrorfor one deliberately mismatched tree. - Run Playwright against production assets, then repeat the native form scenario in a separate
javaScriptEnabled: falsecontext so the ordinaryPOST→303→GETfallback stays executable.
The runnable map is documented in
@fluojs/react and
examples/react-vite-ssr.
These layers already compose the real HTTP dispatcher and application page renderer, so a synthetic
React test runtime would reduce coverage rather than remove necessary setup.
Public API
- Root package:
createTestingModule(...),Test.createTestingModule(...),createTestApp(...), module introspection helpers, and shared app/module testing types includingDeepMocked<T> - Subpaths:
@fluojs/testing/app,@fluojs/testing/module,@fluojs/testing/http,@fluojs/testing/mock(includingDeepMocked<T>),@fluojs/testing/types(includingDeepMocked<T>),@fluojs/testing/vitest,@fluojs/testing/vitest/tooling - Harness subpaths:
platform-conformance,platform-shell-lifecycle-conformance,http-adapter-portability,web-runtime-adapter-portability,fetch-style-websocket-conformance. The HTTP portability harnesses exposeassertSupportsConditionalRequests(),assertSupportsCustomHttpRouteMethods(),assertSupportsSingleByteRanges(),assertSupportsHttpErrorRepresentations(),assertDoesNotCommitAbortedHttpErrorRepresentations(),assertSupportsPortableResponseCookies(),createConditionalRequestBootstrapOptions,createErrorRepresentationBootstrapOptions,NetworkHttpErrorRepresentationBootstrapOptions, andWebHttpErrorRepresentationBootstrapOptionsfor adapter-owned bootstrap typing. - Tooling:
@fluojs/testing/vitestwithfluoBabelDecoratorsPlugin()and@fluojs/testing/vitest/toolingwith Vitest workspace config helpers (requiresvitestand@babel/corein the consuming workspace)
The package manifest declares engines.node >=24.0.0 <27, matching the verified Node listener windows used by its public body-bearing RFC QUERY portability assertion. Node versions below 24 and Node 27+ are excluded. Non-Node runtime application tests can still use runtime-native tools where documented, but the published @fluojs/testing package itself is governed by that exact Node.js engine range.
@fluojs/testing/vitest/tooling maps workspace aliases only for each package's declared public exports. Private source files, internal helpers, and unexported source entrypoints are intentionally excluded so tests exercise the same import boundaries that consumers receive from published packages.
Related Packages
@fluojs/di: powers provider resolution in compiled test containers@fluojs/runtime: provides the module graph behavior that testing builds on@fluojs/http: powers request dispatch used bycreateTestApp()
Example Sources
packages/testing/src/module.test.tspackages/testing/src/portability/error-representation-portability.tsexamples/minimal/src/app.test.tsexamples/auth-jwt-passport/src/app.test.ts
