Test tools
This page documents createTestApp, TestApp, TestRequest, TestRequestBuilder, TestResponse, and createTestJobScheduler. See the Testing guide for a complete business fixture and execution steps; use this page for signatures, defaults, and execution boundaries.
Overview
Import runtime values and types through vextjs/testing:
ESM import and CommonJS require() are supported, and the root and subpath exports share runtime identity. For example, errors can be checked with require("vextjs").HttpError. The root exports only five of the HTTP testing types additionally; see Type import.
- In-memory request: Builds an adapter handler and simulates Node request/response without listening on TCP. Plugins, dictionary scripts, services, and outbound fetch may still perform real I/O.
- Test defaults: Silent logger, disabled rate limiting and access logs, and a one-second shutdown budget; all can be overridden.
- Execution timing: A builder is PromiseLike;
awaitor.then()dispatches the request. - Cleanup:
_testModeis forced to true, so shutdown does not callprocess.exit; callclose()even after successful creation. - Scope: This does not equal CLI bootstrap, real HTTP, frontend rendering/hydration, or database integration.
createTestApp
createTestApp creates a test app and request handler. It does not supply /health, business routes, or a database automatically.
Application dictionaries load before plugins, services, and routes. The default rootDir/src/locales can be overridden with config.locale.directory; module subdirectories, JSON, and script dictionaries are supported. Missing directories give an empty dictionary, invalid formats fail initialization, and scripts execute as modules. This does not read project configuration files, run a configuration provider, or automatically connect a database. To reload explicitly, call the root export loadI18n(testApp.app, directory); see Internationalization.
Function signature
Basic usage
This standalone example needs no business route. Save it as test/testing-api.mjs and run node test/testing-api.mjs from a project with vextjs installed:
It should exit without assertion failures. The option and request fragments below are independent: import their needed types/assertion tools and prepare their routes first. /users is not built in. Close each created app; do not overwrite a variable repeatedly and lose an earlier instance.
Return value
Returns Promise<TestApp>, containing three members: app, request and close.
CreateTestAppOptions
Configuration options for createTestApp. All fields are optional.
Field description
config
Patch framework and test defaults using the same path-aware deep-merge semantics
as profile/local configuration. Nested plain objects already present in those
defaults may be partial; atomic adapters, stores, callbacks, and arrays remain
complete values. createTestApp() does not load the project's
src/config/default.ts, and its built-in defaults do not include database, so
adding that optional section requires a complete database configuration rather
than a partial patch. This helper still does not start the built-in MonSQLize plugin; plugins: true scans only user plugins. See the Database guide for real database verification.
Effective test defaults (unlisted fields inherit framework defaults; session.enabled: false comes from the framework layer):
_testMode is forced true after merging and cannot be disabled through options. Merge priority from lowest to highest is DEFAULT_CONFIG → test defaults → config parameters. Explicitly setting rateLimit.enabled, accessLog.enabled, or session.enabled to true registers the same built-in runtime used by production and development.
TestRequest invokes the built handler directly, so its requests never bind or connect to this port.
plugins
Controls whether to automatically scan the src/plugins/ directory to load plugins.
setupPlugins
Run a manual setup callback instead of automatic plugin scanning. It can extend the app, register global middleware, and register lifecycle hooks. Injected resources are not automatically given a close handler.
setupPlugins is used to replace automatic scanning: when setupPlugins is passed in, the test tool only executes this function and no longer reads the file system scan triggered by plugins: true. If you need real plug-ins, please use plugins: true; if you need precise control of test dependencies, please only use setupPlugins.
services
Controls whether to automatically load services in the src/services/ directory.
TypeScript service file loading mechanism
Service and user-middleware TypeScript modules use a shared module loader that compiles local dependencies and maps .js imports to .ts source. npm dependencies still resolve from the project. Temporary products have a project owner and are cleaned afterward. The working directory must be writable; missing modules and invalid exports fail initialization.
Route files instead load through Node's direct import(fileURL). Do not assume that Service compilation automatically compiles routes. On Node 20, an installed package plus Vitest may report Unknown file extension ".ts"; use JavaScript routes, a compatible test loader, or a Node environment satisfying native TS type stripping as described in Testing: Quick start. The real CLI dev/build path has its own compilation flow.
Multiple test processes sharing the same or overlapping rootDir may contend for the owner and report VEXT_OWNER_BUSY. Run a shared fixture serially or use separate, nonoverlapping projects. Lack of a TCP port conflict does not imply unconditional parallel safety.
mockServices
Manually inject the mock service and overwrite the service-loader scan results.
This leaves services at its default true. Add services: false when only the mock should run, avoiding construction of the real Service first. A mock does not automatically implement missing business methods; it must satisfy the methods your routes use.
Merge Logic:
routes
Controls whether to load rootDir/src/routes/. The request chain is prepared from final configuration and middleware allowlist before loading; file prefixes still apply. routes: false does not register a test route.
middlewares
Controls loading user middleware from rootDir/src/middlewares/ according to config.middlewares. Default true does not enable the whole directory; an empty allowlist loads none, and routes referring to undeclared names fail.
This option controls only the user-middleware directory. Built-in requestId, CORS, bodyParser, response wrapper, Session, CSRF, and rate limiting follow their own configuration; they are not all unconditionally registered.
rootDir
The project root directory is used to locate src/routes, src/services, src/plugins and other directories.
devOverlay
Pass (error: unknown) => string to render an HTML error when request Accept includes text/html. If the callback throws, normal error handling takes over. This does not start frontend builds, Fast Refresh, or a browser, and it does not affect JSON requests. Test error hiding with an explicit Accept header and response.hideInternalErrors setting.
Initialization and resource boundary
Order: configuration merge → app / i18n / Session / rateLimit runtime / adapter / fetch → plugins → user middleware → Services → mock override → routes → built-in request chain and error handling → onReady → handler / TestRequest.
onReady is awaited, but an individual hook error is logged by the app and execution continues; do not assume initialization rejects. Assert readiness side effects directly. This helper omits production bootstrap's config files, providers, preload, built-in DB, frontend artifact checks, and process signal flow.
On initialization failure, no close() has been returned. Custom setup that fails after allocating resources must clean up what it acquired; do not assume every failure path was rolled back as a whole.
TestApp
The test application instance returned by createTestApp.
app
The underlying VextApp instance, which can be used to directly access application capabilities:
request
HTTP request simulator, similar to supertest style API. See TestRequest for details.
close()
Close the test application, trigger the onClose hook, and clean up resources.
Be sure to call close() in afterEach or afterAll, otherwise it will cause resource leaks (database connections, timers, etc.) and the test process cannot exit.
close() waits for app shutdown. The default one-second budget applies to the whole shutdown, not each hook. Closing an already closed instance does not repeat cleanup. It does not reset all process-level module caches or delete external test data.
TestRequest
HTTP request simulator, providing a chained API similar to supertest style.
Supported HTTP methods
Each method returns TestRequestBuilder, which supports chain configuration and execution of requests through await.
request.head(path) follows HTTP HEAD semantics. The response status and headers are preserved, but TestResponse.text and TestResponse.body are empty even when the route handler writes a body. Use the matching GET route when you need to assert the response body.
Basic usage
TestRequestBuilder
The chained request constructor supports setting request headers, query parameters, request bodies, etc., and finally executes the request through await or .then().
TestRequestBuilder is PromiseLike, so await executes a request without .execute(). Each await or .then() runs a new request; the builder does not cache a Promise result and does not provide the full Promise catch/finally interface.
set(key, value)
Set one request header. Names are lowercased, and setting the same name again replaces its previous value.
headers(headers)
Set multiple request headers (in object form).
set() and headers() can be used together, and the value set later will overwrite the request header with the same name set first.
query(params)
Set URL query parameters.
Parameter values are automatically converted to strings and URL encoded.
Calling query() again replaces the previous object:
An existing query string in path remains and the new query object is appended with &; duplicate keys may result and are parsed by application rules. The type does not accept arrays; encode them in the path yourself. Multiple query() calls do not create multi-value parameters.
send(body)
Set or replace the request body. Strings are sent as-is; other non-undefined values use JSON.stringify. If no Content-Type is supplied, the default is application/json. Buffer, Stream, and FormData are not automatically encoded as uploads; use a real HTTP client for those transports.
type(contentType)
Set the Content-Type request header.
send() defaults to Content-Type: application/json if unset. type() works before or after send() and takes precedence over Content-Type set through set() or headers() when executing. If send() created the default type, a later set() does not override that separate type value; use type(). A Content-Type declaration does not encode an object as form-urlencoded or XML.
Chain combination
All methods support chained calls, and the request is ultimately executed through await:
TestResponse
Simulates an HTTP response object, including status code, response headers and parsed response body.
status
HTTP status code.
headers
Response header object, all keys are lowercase.
Multiple Set-Cookie values are preserved without comma splitting. The following two-cookie assertion assumes that the route actually sets two cookies. Prefer res.cookies or res.headerValues("set-cookie"):
cookies / header(name) / headerValues(name)
These helpers do not maintain a session automatically. To continue one, send the relevant Set-Cookie name=value pairs in a later Cookie request header and keep test sessions isolated.
body
When Content-Type includes application/json or +json, JSON parsing is attempted; the result can also be a primitive or null. For other types or parse failure, body retains the same string as text, not undefined. HEAD and 204 empty bodies are usually "".
Error response:
With business error code:
text
Raw response text. For a JSON response, text is the JSON string; for a text response, text is the raw text content.
createTestJobScheduler
Definitions are explicitly supplied rather than scanned from src/jobs. Object names prefer definition.name then the key; arrays fall back to job1/job2. Duplicates fail. Other CreateTestAppOptions apply, with routes: false; plugins and services retain their initialization side effects.
now defaults to the current time and registers strictly future points. tick(Date) waits for handlers admitted at that point; repeated ticks do not duplicate execution. Missed periods are not replayed and same-job overlap is skipped. Retain a long first tick's Promise while advancing another tick to test overlap. Handler failures are logged; use business assertions to verify outcomes.
close() stops triggers, requests cancellation and waits within the total app shutdown budget before dependency cleanup. The helper starts no real timers, but configured Redis is connected, validated and used. Redis test points must match the server's current period. Ordinary createTestApp() does not schedule jobs. See the Jobs example.
Usage mode
Runnable CRUD, middleware, mock, and Service unit tests are in Testing examples. Adapter transport, SSE, WebSocket, uploads, socket disconnects, and real TLS need separate network verification.
Best Practices
- Close a successfully created app in
finallyorafterEach/afterAll. UseTestApp | undefinedto handle creation failure. - Give stateful tests independent data. Read-only cases can share an instance, but serialize module loading for a shared
rootDir. - Constrain mocks to real public interfaces. A plain Error's valid
status/statusCodecan also be normalized; useHttpErrororapp.throwfor business codes and type identity. - Assert the target branch explicitly. “Status is not 401” can misread a 500 as success. State input conditions and expected results in test names.
- Use real CLI verification for production config, preload, DB, frontend, and build/start.
Common issues
Type import
Import runtime testing values through vextjs/testing. The five HTTP types above may come from the main vextjs entry. Import TestResponseHeaderValue, CreateTestJobSchedulerOptions, and TestJobScheduler from vextjs/testing; do not import the createTestApp or createTestJobScheduler runtime values from the root entry.