Skip to main content

TestRunner

TestRunner is the primary testing API. It boots your application (or library) in an isolated test environment and gives you typed access to all services.

import { TestRunner } from "@digital-alchemy/core";
import { MY_APP } from "./application.mts";

const runner = TestRunner(MY_APP);

Constructor​

function TestRunner<S, C>(options?: CreateTestingLibraryOptions<S, C>): iTestRunner<S, C>

options can be omitted (creates an empty test app) or can specify a target (library or application to test). When passing the application directly: TestRunner(MY_APP) is shorthand for TestRunner({ target: MY_APP }).

By default:

  • Logs are suppressed (noop logger) — use .emitLogs() to enable
  • Environment variables and config files are not loaded — use .configure() to provide config
  • Process max listeners is set to unlimited to avoid EventEmitter warnings in test suites

Fluent builder methods​

All methods return this for chaining. Multiple calls to methods that accept objects are deep-merged.

.configure(config)​

Set config values for this test run. Deep merges with any previous .configure() calls.

runner.configure({ my_app: { PORT: 9999, DEBUG: true } });

.setOptions(options)​

Set TestingBootstrapOptions directly. Deep merges.

OptionDefaultDescription
emitLogsfalseEnable log output for this test
loggerOptions—Fine-tune logger output
loadConfigsfalseLoad env vars and config files
customLoggernoopCustom logger implementation
bootLibrariesFirstfalseBoot libraries before wiring app
configuration—Same as .configure()
configSources—Enable/disable specific loaders

.emitLogs(level?)​

Enable log output for this test. Optionally set the log level:

runner.emitLogs("debug");

Useful when debugging a failing test — add it temporarily to see what's happening.

.bootLibrariesFirst()​

Sets bootLibrariesFirst: true for this test.

.configure(config)​

Config override. Deep merges with previous calls.

.appendLibrary(library)​

Add a library that wasn't in the original app's libraries array. The added library is available to all services.

runner.appendLibrary(MOCK_HTTP_LIB);

.appendService(service, name?)​

Add an extra service to the app. By default uses service.name as the service key; provide name to override.

runner.appendService(TestHelperService);
runner.appendService(AnonHelper, "helper");

.replaceLibrary(name, library)​

Swap a library (by name string) for a different implementation. The replacement must have the same name.

runner.replaceLibrary("database", MOCK_DATABASE_LIB);

.setup(serviceFn)​

Register a service function to wire into a run_first library that wires before the target module. Multiple calls add multiple services; all run before the main target.

runner.setup(async ({ my_app }) => {
// pre-populate data before the test
my_app.registry.add("test-item", { id: 1 });
});

Terminal methods​

.run(testFn)​

Boots the application, runs testFn with TServiceParams, then tears down. Returns the bootstrapped application.

await TestRunner(MY_APP).run(async ({ my_app }) => {
expect(my_app.counter.value).toBe(0);
my_app.counter.increment();
expect(my_app.counter.value).toBe(1);
});
// Teardown happens automatically

.serviceParams()​

Boots the application and returns TServiceParams without tearing down. You must call .teardown() manually.

const params = await TestRunner(MY_APP).serviceParams();
expect(params.my_app.counter.value).toBe(0);
await params.teardown(); // required

Wait — serviceParams() returns TServiceParams, which does not have a teardown property. The teardown comes from the runner:

const runner = TestRunner(MY_APP);
const params = await runner.serviceParams();
// use params...
await runner.teardown();

.teardown()​

Runs the full shutdown sequence. Safe to call if the app is not booted (no-op). Always call this in afterEach when using .serviceParams().

Typical patterns​

Simple test with .run():

it("does the thing", async () => {
await TestRunner(MY_APP).run(async ({ my_app }) => {
expect(my_app.service.doThing()).toBe("expected");
});
});

Manual teardown for shared setup:

describe("MyService", () => {
let teardown: () => Promise<void>;

beforeAll(async () => {
const runner = TestRunner(MY_APP);
const params = await runner.serviceParams();
// store params for tests...
teardown = () => runner.teardown();
});

afterAll(() => teardown?.());
});

Chained with mocking:

await TestRunner(MY_APP)
.replaceLibrary("database", MOCK_DB_LIB)
.configure({ my_app: { PORT: 9999 } })
.emitLogs("debug")
.run(async ({ my_app }) => {
// ...
});