Skip to main content
Talos applications are tested with Bun’s test runner. There is no separate test framework to install or configure. Tests import from bun:test, live in a tests/ directory next to the code they cover, and follow the *.spec.ts naming convention. Because every service, middleware, and controller is a plain class resolved through the dependency injection container, you test units in isolation: construct them directly (or resolve them from a fresh Container) and hand them a mock context.

What testing looks like here

bun test discovers and runs specs with no config file. The API is Jest-compatible (describe/test/expect) and runs at native speed. Since middleware, controllers, and services are constructor-injected classes, you can new them with fakes or resolve them from a container without booting the app. The DI container is the lever for isolation. Register test doubles in place of real services and the unit under test sees a fake DB, cache, or mailer instead of the real thing. A dedicated testing environment (APP_ENV=testing) keeps configuration and behavior separate from local, staging, and production. And because every CLI generator scaffolds a matching test file next to the artifact it creates, new code starts with a spec already in place.

How it works

Running tests

Run the whole suite from the repository or package root:
bun test automatically loads .env files, so the same configuration loading your app uses is available to tests. Set APP_ENV=testing to run against the testing environment (see Using the testing environment).

Test file conventions

A spec imports the helpers it needs from bun:test, the unit under test through the @/ alias, and any framework packages it depends on:
Group related cases in a describe block, write one assertion-focused test per behavior, and use beforeEach/afterEach to set up and tear down shared state:

Unit testing a middleware

Middleware and controllers receive a context object. In a unit test you don’t boot the HTTP server. Instead you build a minimal mock context exposing only the fields the unit reads (method, header.get, response.header.*, response.json, and so on) and assert on what the handler wrote back. This pattern is adapted from the real CorsMiddleware spec. A small factory builds the mock context and records the headers the middleware sets:
The same shape works for controllers: build a mock context with the request fields the handler reads, call the handler, and assert on the response it produced. See Middleware and Controllers for the real context interface.

Testing with the container

When a class is registered through dependency injection, resolve it from a fresh container per test so registrations from one test never leak into another. Create the container in beforeEach, register the class, and get it back:

Registering test doubles

To isolate a unit from external systems (database, cache, mailer), register a fake under the same key the real binding uses. container.add(...) rebinds a class, and container.addConstant(key, value) binds a ready-made value:
For the full container API (scopes, constants, and injection) see Dependency injection.

Using the testing environment

Talos recognizes a dedicated testing environment alongside local, development, staging, and production. It is selected by the APP_ENV variable:
With APP_ENV=testing, an AppEnv instance reports isTesting === true, and any environment-specific configuration loads its testing values. Drive a code path that checks the environment by setting APP_ENV before constructing AppEnv:
Config and env load in tests exactly as they do at runtime, since Bun reads .env files automatically, so prefer a dedicated .env.testing (or test-only values) over hardcoding secrets in specs. See Configuration for how environment values are resolved.

Generated test stubs

Every CLI generator scaffolds a matching test next to the artifact it creates. For example, talos middleware:create --name=Auth writes the middleware class and a companion spec in the module’s tests/ directory, pre-wired with the bun:test imports and a describe block. New code starts testable, so you fill in the cases rather than setting up the file from scratch.

Keeping the suite trustworthy

Isolation is the thing to protect. Build fresh state in beforeEach, tear it down in afterEach, and never let one test’s env vars or container bindings reach the next. A new Container() per test keeps registrations from leaking across cases. Replace the database, cache, mailer, and HTTP clients with fakes registered in the container so a test never hits a real system. Mock the context narrowly: build only the fields the unit reads, and record what it writes so the assertions stay focused. Run with APP_ENV=testing and a test-only env file rather than reusing local or production configuration. Write one behavior per test, name it for what it proves, and remove redundant or trivial cases so the suite stays a fast, trustworthy signal.