Testing
createTestApp() from vextjs/testing runs route, middleware, and Service request chains in memory. It does not listen on TCP or run the full CLI startup flow. Database connections, config providers, production builds, and frontend hydration need their own real integration checks.
Both ESM import and CommonJS require() work. The package root and public subpaths share runtime identity; for example, an HttpError thrown by a test app can satisfy instanceof require("vextjs").HttpError. See Testing API for the full return types.
Quick Start
Prerequisite: use the TypeScript API-only project from Quick Start, retaining the scaffold's /health route in src/routes/index.ts and its Service. Install Vitest if it is not already present:
The TS route example on this page requires Node.js 22.18+ or a newer version with equivalent default type stripping, plus "type": "module" in package.json. Route loading uses Node import directly: installing Vitest alone does not make Node 20 load .ts routes. Native type stripping neither remaps .js to .ts at runtime nor reads path aliases, and it does not support every TypeScript syntax form. See the Node TypeScript documentation. For complex route dependencies, configure a compatible test loader or test real CLI build output.
Create this test file and run it from the project root. Check the current Vitest Node requirement in its official installation guide, and use a Node version supported by both tools.
Expect the test to pass; removing or renaming the route should make it fail, and restoring it should pass again. The full-stack scaffold uses /api/health, so do not copy the API-only path there. This test uses the Native adapter. An application using another adapter must also verify its installed, actual adapter.
The helper is independent of Vitest and can be used with Node's test runner or Jest. Standalone cases below close resources; small API snippets assume an existing t: TestApp that is closed with await t.close() at the end of its test.
createTestApp()
createTestApp() creates a test application, loads explicitly selected modules, and constructs an in-memory request handler. It returns { app, request, close }: t.app is a VextApp, t.request sends requests, and t.close() releases resources. There is no app.inject().
Basic usage
By default, routes and services under this root load; middleware also needs a configured allowlist. Locales load before initialization, and the helper awaits onReady before returning. Plugins, Services, locale scripts, and app.fetch can still contact external systems. No HTTP listening does not mean no I/O or side effects.
Configuration options
CreateTestAppOptions.config is an override layer, not a standalone base
configuration. VextConfigOverride lets a test patch nested fields already
present in the framework/test defaults. Atomic adapters, stores, callbacks, and
arrays still have to be supplied as complete values.
createTestApp() does not load the project's src/config/default.ts. The built-in test defaults do not define database, so a test that adds that optional section must provide complete database configuration, including required connection config; a partial database section has no earlier layer to complete it. Even with complete config, this helper does not automatically run the built-in database plugin. Check real database behavior through the CLI path described below.
Dictionaries load before plugins, services, and routes. The default is rootDir/src/locales; set config.locale.directory for a custom directory using the normal startup resolver. Module subdirectories, JSON, and script dictionaries are supported; scripts execute. A missing directory gives an empty dictionary, while an invalid dictionary fails test application initialization. This step does not load project configuration files, execute a configuration provider, or automatically connect a database.
If setupPlugins is supplied, its callback replaces directory scanning even when plugins: true; both do not run. With services: true and mockServices, real Services are constructed before the mocks replace them, so constructor side effects have already happened. Use services: false to bypass real loading.
Common configuration scenarios
Test defaults and log level
Skip plugin loading
Simulation service
Custom plugin
These are option snippets; close every TestApp you create. mockServices only overrides service objects: it does not create routes, authentication, or a database. Supply every method the route actually uses. In a project with generated Service types, implement the public mock interface rather than hiding missing methods with as any.
Send test request
The object returned by createTestApp() contains the request attribute and supports all HTTP methods:
Chained build requests
Each HTTP method returns a TestRequestBuilder for configuring the request. The request runs only when you await it or call .then(). Awaiting the same builder twice sends two requests; it does not reuse a response:
.set(name, value) — Set a single request header
.headers(obj) — Set request headers in batches
.query(obj) — Set URL query parameters
.send(body) — Set the request body
.type(contentType) — Set Content-Type
.send() passes a string through unchanged and JSON-stringifies other values. Setting Content-Type does not encode forms or multipart automatically. The builder has no file attachment, Cookie jar, or automatic redirect following. Verify binary, streaming, disconnect, and full upload behavior over real HTTP.
TestResponse response object
A TestResponse object is returned after the request is completed:
Read multiple Set-Cookie values with cookies or headerValues(); do not split on commas. Set the Cookie header manually for the next request. HEAD has an empty text, so it cannot be asserted to have the same body as GET.
Test mode features
The application created by createTestApp() is in test mode (_testMode: true), which has the following differences from production mode:
The helper forces _testMode: true and does not register production signal or fatal-error handlers. Test defaults disable access logs, and rate limiting must be enabled explicitly to exercise a 429 path. Its config merge differs from CLI file loading, provider, validation, and preload, so passing a helper test does not prove production config works.
Practical example
Prepare isolated routes and services
These independent fixtures live under test/fixtures/http/src/ and do not mix with application routes. Service data is in memory per instance. The token is test input, not a production authentication design.
The filename prefix gives /items and /items/:id. Configure the middleware allowlist below: creating token.ts alone does not enable it. A real application's Service types should normally be generated by typegen; this fixture explicitly constrains the four methods used by its route.
Test CRUD routes, middleware, and errors
Expect five passing cases. Each gets a fresh Service instance, so the delete case does not pollute later lists. afterEach closes every successfully created TestApp. The 401 assertions send valid bodies, while the 422 assertion sends a valid token; this isolates the intended rejection instead of mistaking an earlier rejection for coverage.
Test with mock services
Reuse the fixture above, replacing only its Service in a separate test file:
Use real app.throw(...) or public HttpError for business errors. Error normalization may also read a valid status or statusCode on an ordinary Error, but that object lacks HttpError type, name, and business-code contracts; an error without a valid HTTP status defaults to 500. A passing mock test checks the route-to-mock contract, not the real database or Service implementation.
Unit-test the service layer
This fixture's Service has no app dependency and can be instantiated directly:
For a business Service that depends on VextApp, obtain a real base app from a TestApp with scanning disabled or provide a typed mock for its actual dependencies. Do not use as any to conceal missing app capabilities.
Project configuration
TypeScript service files and ESM loading
With services: true, the helper scans rootDir/src/services/. The shared module loader compiles TypeScript before import, handling type stripping and local .js imports pointing to .ts sources. npm dependencies still resolve from the project. The project must permit temporary compiled outputs to be created and cleaned up; a missing dependency or a default export that cannot be constructed fails initialization.
This does not mean every Node/Vite version cannot load TS, nor does it guarantee compatibility with dynamically assembled paths or all third-party loaders. For route-only contracts, services: false with mockServices bypasses Service loading. Keep the real chain when checking construction, cross-Service dependencies, or production artifacts.
Vitest configuration
When multiple test files use overlapping rootDir paths, module loading contends for the same project owner. Add this vitest.config.ts before running all examples to keep those files serial. Restore parallelism only for genuinely separate fixtures with nonoverlapping roots:
Coverage needs a compatible @vitest/coverage-v8; see official coverage guidance. Vitest normally transforms code rather than type-checking the full project. Run type checking separately and include test/ in its dedicated tsconfig. For modules compiled through the additional filesystem loader, check whether coverage paths map back to sources instead of trusting only an aggregate percentage.
Test directory structure
A suggested layout is:
package.json scripts
Best Practices
1. Choose a level for the subject under test
2. Close resources and isolate state
Test logs are silent by default. A read-only suite can create an app in beforeAll and close it in afterAll. For mutable state, prefer beforeEach/afterEach or a fresh mock per test so results do not depend on order. Each TestApp's onClose hooks release its resources; the default timeout is one second, so explicitly adjust it for longer cleanup.
Do not share an app, Store, or database collection with mutable data across concurrent tests. Loads from the same or overlapping rootDir also share a project owner and may report VEXT_OWNER_BUSY; serialize as above or use truly separate project roots. Pass Cookies manually per test session. External databases, Redis, pools, and background Jobs are not isolated automatically by _testMode.
3. Verify the intended branch and side effect
Check response shape, headers, mock arguments, and important side effects as well as status. Negative inputs should pass earlier authentication or validation before reaching the target branch. For example, an invalid body without a token does not prove Schema validation ran. Check the intended error instead of merely asserting that some error occurred; a load failure or 500 is not the expected business result.
4. Complete production-path checks with the real CLI
From the business project root, start the real service:
From another terminal, request actual business URLs and check success, invalid input, unauthenticated access, dependency failure, and recovery. Stop dev, then:
Repeat business requests and inspect the config profile, dependency connections, and logs. A JavaScript API-only project does not need a backend build; follow Build for its path. The test/fixtures above are only for the helper and do not automatically become the CLI project's src.
For external MongoDB integration, prepare an isolated database and verification profile using Database. Check frontend SSR, hydration, and refresh using Frontend Overview. Stop services you started for testing and clean up data you created; preserve existing user services and data of unknown ownership.
Testing Jobs
Ordinary createTestApp() does not schedule src/jobs. Supply definitions and an initial now to createTestJobScheduler(), advance with tick(Date), and close in teardown. It verifies future points, overlap skipping and failure behavior without real timers. Multi-replica coordination also needs real Redis integration tests. See Jobs API.
Next step
- Learn how routing defines a testable API
- Learn the unit testing pattern of Service Layer
- See CLI Commands to learn about
dev/build/startand other running commands - Explore testing tips for middleware