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.
Why this approach
- No extra tooling.
bun testdiscovers and runs specs out of the box — Jest-compatibledescribe/test/expectwith native speed. - Plain classes. Middleware, controllers, and services are constructor-injected classes. You can
newthem with fakes, or resolve them from a container — no app boot required. - Container-driven doubles. The DI container lets you register test doubles in place of real services, so a unit under test sees a fake DB, cache, or mailer.
- A dedicated environment. The
testingenvironment (APP_ENV=testing) isolates configuration and behavior from local, staging, and production. - Generated stubs. The CLI scaffolds a matching test file next to every artifact it generates, so new code starts with a test in place.
How it works
| Convention | Detail |
|---|---|
| Test runner | Bun’s built-in runner — bun test, no config file needed. |
| Location | A tests/ directory inside each package or module. |
| File naming | *.spec.ts (e.g. CorsMiddleware.spec.ts). |
| Test API | import { describe, expect, test, beforeEach, afterEach } from "bun:test". |
| Imports | Use the @/ path alias for the unit under test, @ooneex/* for framework packages. |
| Isolation | A fresh new Container() and/or mock context per test; reset env in afterEach. |
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 frombun:test, the unit under test through the @/ alias, and any framework packages it depends on:
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 acontext object. In a unit test you don’t boot the HTTP server — you build a minimal mock context exposing only the fields the unit reads (method, header.get, response.header.*, response.json, …) 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:
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 inbeforeEach, 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:
Using the testing environment
Ooneex recognizes a dedicatedtesting environment alongside local, development, staging, and production. It is selected by the APP_ENV variable:
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:
.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,ooneex 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 — fill in the cases rather than setting up the file from scratch.
Best practices
- Isolate every test. Use
beforeEachto build fresh state andafterEachto tear it down; never let one test’s env vars or container bindings reach the next. - Fresh container per test. Create
new Container()inbeforeEachso registrations don’t leak across cases. - Mock external services. Replace the database, cache, mailer, and HTTP clients with fakes registered in the container — tests must not hit real systems.
- Mock the context minimally. Build only the
contextfields the unit reads, and record what it writes so assertions stay focused. - Use the testing environment. Run with
APP_ENV=testingand a test-only env file rather than reusing local or production configuration. - Keep tests meaningful. One behavior per
test, named for what it proves; remove redundant or trivial cases so the suite stays a fast, trustworthy signal.