> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ooneex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

> Test an Ooneex application with Bun's built-in test runner, resolving services from the container and passing mock contexts to middleware and controllers.

Ooneex applications are tested with [Bun's built-in test runner](https://bun.sh/docs/cli/test) — 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`.

## Why this approach

* **No extra tooling.** `bun test` discovers and runs specs out of the box — Jest-compatible `describe`/`test`/`expect` with native speed.
* **Plain classes.** Middleware, controllers, and services are constructor-injected classes. You can `new` them 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 `testing` environment (`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:

```bash theme={null}
# Run every *.spec.ts in the project
bun test

# Re-run on file changes
bun test --watch

# Run a single file
bun test packages/middleware/tests/CorsMiddleware.spec.ts

# Filter by test name (substring or pattern)
bun test --test-name-pattern "CORS_ORIGINS"

# Only run tests under a directory
bun test packages/middleware
```

`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](#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:

```typescript theme={null}
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { CorsMiddleware } from "@/CorsMiddleware";
```

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:

```typescript theme={null}
describe("CorsMiddleware", () => {
  afterEach(() => {
    // Clean up any env vars a test set, so the next test starts clean
    delete Bun.env.CORS_ORIGINS;
  });

  test("defaults to wildcard origin when CORS_ORIGINS is unset", async () => {
    // ...
  });
});
```

## Unit testing a middleware

Middleware and controllers receive a `context` 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:

```typescript theme={null}
import { afterEach, describe, expect, test } from "bun:test";
import { AppEnv } from "@ooneex/app-env";
import { CorsMiddleware } from "@/CorsMiddleware";

function createMockContext(options: { origin?: string; method?: string } = {}) {
  const responseHeaders = new Map<string, string>();

  return {
    method: options.method ?? "GET",
    header: {
      get: (name: string) => (name === "Origin" ? (options.origin ?? null) : null),
    },
    response: {
      header: {
        setAccessControlAllowOrigin(origin: string) {
          responseHeaders.set("Access-Control-Allow-Origin", origin);
          return this;
        },
        set(name: string, value: string) {
          responseHeaders.set(name, value);
          return this;
        },
      },
      // biome-ignore lint/suspicious/noExplicitAny: mock
      json(_data: any, _status?: number) {},
    },
    _responseHeaders: responseHeaders,
    // biome-ignore lint/suspicious/noExplicitAny: mock
  } as any;
}

describe("CorsMiddleware", () => {
  afterEach(() => {
    delete Bun.env.CORS_ORIGINS;
  });

  test("defaults to wildcard origin when CORS_ORIGINS is unset", async () => {
    const middleware = new CorsMiddleware(new AppEnv());
    const context = createMockContext({ origin: "https://anything.com" });

    await middleware.handler(context);

    expect(context._responseHeaders.get("Access-Control-Allow-Origin")).toBe("*");
  });

  test("restricts to configured origins", async () => {
    Bun.env.CORS_ORIGINS = "https://example.com";
    const middleware = new CorsMiddleware(new AppEnv());

    const disallowed = createMockContext({ origin: "https://evil.com" });
    await middleware.handler(disallowed);

    expect(disallowed._responseHeaders.size).toBe(0);
  });
});
```

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](/basics/middleware) and [Controllers](/basics/controller) 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:

```typescript theme={null}
import { beforeEach, describe, expect, test } from "bun:test";
import { Container, EContainerScope } from "@ooneex/container";

describe("container resolution", () => {
  let container: Container;

  beforeEach(() => {
    container = new Container();
  });

  test("resolves a transient class as a new instance each time", () => {
    class ReportService {}

    container.add(ReportService, EContainerScope.Transient);

    expect(container.get(ReportService)).not.toBe(container.get(ReportService));
  });
});
```

### 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:

```typescript theme={null}
import { beforeEach, describe, expect, test } from "bun:test";
import { Container } from "@ooneex/container";
import { UserService } from "@/UserService";

class FakeMailer {
  public readonly sent: string[] = [];
  public async send(to: string) {
    this.sent.push(to);
  }
}

describe("UserService", () => {
  let container: Container;
  let mailer: FakeMailer;

  beforeEach(() => {
    container = new Container();
    mailer = new FakeMailer();
    // Bind the fake in place of the real mailer
    container.addConstant("Mailer", mailer);
    container.add(UserService);
  });

  test("emails the user on registration", async () => {
    const service = container.get(UserService);

    await service.register("apps@ooneex.com");

    expect(mailer.sent).toContain("apps@ooneex.com");
  });
});
```

For the full container API — scopes, constants, and injection — see [Dependency injection](/advanced/dependency-injection).

## Using the testing environment

Ooneex recognizes a dedicated `testing` environment alongside `local`, `development`, `staging`, and `production`. It is selected by the `APP_ENV` variable:

```bash theme={null}
APP_ENV=testing bun test
```

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`:

```typescript theme={null}
import { afterEach, expect, test } from "bun:test";
import { AppEnv } from "@ooneex/app-env";

afterEach(() => {
  delete Bun.env.APP_ENV;
});

test("AppEnv reports the testing environment", () => {
  Bun.env.APP_ENV = "testing";
  const env = new AppEnv();

  expect(env.isTesting).toBe(true);
  expect(env.APP_ENV).toBe("testing");
});
```

Config and env load in tests exactly as they do at runtime — Bun reads `.env` files automatically — so prefer a dedicated `.env.testing` (or test-only values) over hardcoding secrets in specs. See [Configuration](/getting-started/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.

```bash theme={null}
ooneex middleware:create --name=Auth
# -> src/middlewares/AuthMiddleware.ts
# -> tests/AuthMiddleware.spec.ts
```

## Best practices

* **Isolate every test.** Use `beforeEach` to build fresh state and `afterEach` to tear it down; never let one test's env vars or container bindings reach the next.
* **Fresh container per test.** Create `new Container()` in `beforeEach` so 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 `context` fields the unit reads, and record what it writes so assertions stay focused.
* **Use the testing environment.** Run with `APP_ENV=testing` and 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.
