---
name: tsed-testing
description: Write unit and integration tests for Ts.ED v8 applications with Vitest (Jest as a fallback) - vitest.config with unplugin-swc, PlatformTest.create/invoke/reset from @tsed/platform-http/testing, provider mocking with {token, use}, inject() in specs, PlatformTest.createRequestContext, and PlatformTest.bootstrap(Server) with SuperTest and PlatformTest.callback(). Use when adding or fixing *.spec.ts files for services, controllers, middlewares or interceptors, when injected properties are undefined in tests, or on the error "Platform type is not specified" / a missing platform adapter.
---

# Test a Ts.ED Application

Build every tested class through the Ts.ED test injector. Never instantiate a DI-managed class (service, controller, middleware, interceptor, module) with `new` in a test: it bypasses injection, scopes, hooks and mocks.

Read [the testing recipes](references/recipes.md) for middleware, interceptor, request-context and stubbing examples. Depth: `https://tsed.dev/docs/testing.md`, `https://tsed.dev/tutorials/vitest.md`.

## 1. Configure Vitest

Install `vitest unplugin-swc @swc/core @vitest/coverage-v8` (and `supertest @types/supertest` for integration tests). A project generated by tsed-cli already has this.

```typescript
// vitest.config.mts
import swc from "unplugin-swc";
import {defineConfig} from "vitest/config";

export default defineConfig({
  test: {globals: true, root: "./"},
  plugins: [
    swc.vite({
      jsc: {
        target: "es2022",
        keepClassNames: true,
        parser: {syntax: "typescript", decorators: true},
        transform: {useDefineForClassFields: false, legacyDecorator: true, decoratorMetadata: true}
      }
    })
  ]
});
```

- Do not rely on Vitest's default esbuild transform: it does not emit decorator metadata, so constructor and `@Inject()` property injection resolve to `undefined`.
- Do not remove `useDefineForClassFields: false`; class fields would overwrite injected properties.
- Jest: use `ts-jest` per `https://tsed.dev/tutorials/jest.md`. It is unstable with ESM; prefer Vitest for new projects.

## 2. Choose the test type

1. Logic of one provider with mocked collaborators: unit test with `PlatformTest.create()` (steps 3-4). Fast, no HTTP server.
2. Routing, validation, serialization, middlewares chain, exception filters: integration test with `PlatformTest.bootstrap(Server)` + SuperTest (step 5).
3. Real database or broker: add a testcontainers package (step 6).

## 3. Unit test a provider

```typescript
import {inject} from "@tsed/di";
import {PlatformTest} from "@tsed/platform-http/testing";
import {afterEach, beforeEach, describe, expect, it} from "vitest";
import {UsersService} from "./UsersService.js";

describe("UsersService", () => {
  beforeEach(() => PlatformTest.create());
  afterEach(() => PlatformTest.reset());

  it("should return the user", async () => {
    const service = inject(UsersService);

    expect(await service.findById("1")).toEqual({id: "1"});
  });
});
```

- `PlatformTest.create(settings?)` accepts any configuration key: `PlatformTest.create({features: {beta: true}})` then read it with `constant("features.beta")`.
- `PlatformTest.get(Token)` and `inject(Token)` return the shared instance. `PlatformTest.injector` is the `InjectorService`.
- Import `inject` from `@tsed/di`. `@tsed/platform-http/testing` only exports `PlatformTest` and `FakeAdapter`.
- For code without HTTP dependencies, `DITest` from `@tsed/di` offers the same `create`, `invoke`, `get`, `reset`.

## 4. Mock dependencies

```typescript
const repository = {findById: vi.fn().mockResolvedValue({id: "1"})};

const service = await PlatformTest.invoke<UsersService>(UsersService, [{token: UsersRepository, use: repository}]);

expect(await service.findById("1")).toEqual({id: "1"});
expect(repository.findById).toHaveBeenCalledWith("1");
```

- `PlatformTest.invoke(Token, [{token, use}])` builds a fresh instance with local mocks and runs `$onInit`. Always `await` it.
- Mock for the whole `describe`: `PlatformTest.create({imports: [{token: UsersRepository, use: repository}]})`, then `inject(UsersService)`.
- `imports` entries accept `use`, `useClass`, `useFactory` or `useAsyncFactory`. `useValue` is ignored there.
- Symbol or factory tokens are mocked the same way: `{token: DbConnection, use: fakeDb}`.
- Controllers are providers: `await PlatformTest.invoke<UsersController>(UsersController, [...])` and call the handler method directly with plain arguments.
- Mock only direct collaborators. Do not `vi.mock()` a class file that the injector must still register.

## 5. Integration test with SuperTest

```typescript
import {PlatformTest} from "@tsed/platform-http/testing";
import SuperTest from "supertest";
import {afterAll, beforeAll, describe, expect, it} from "vitest";
import {Server} from "../Server.js";
import {UsersController} from "./UsersController.js";

describe("UsersController", () => {
  beforeAll(PlatformTest.bootstrap(Server, {mount: {"/rest": [UsersController]}}));
  afterAll(PlatformTest.reset);

  it("should call GET /rest/users/:id", async () => {
    const request = SuperTest(PlatformTest.callback());
    const response = await request.get("/rest/users/1").expect(200);

    expect(response.body).toEqual({id: "1"});
  });
});
```

- `PlatformTest.bootstrap()` returns a function; pass it to `beforeAll`, or call it: `await PlatformTest.bootstrap(Server)()`.
- The platform adapter must be registered: import `@tsed/platform-express` (or `-koa`, `-fastify`) in `Server.ts` or in the spec, or pass `{adapter: PlatformExpress}`.
- No port is opened unless `listen: true` is passed. The environment is `test` and the logger level is `off` by default.
- Settings passed as second argument replace the same `@Configuration` keys (only `mount`, `scopes`, `logger` are merged). Passing `imports` drops the Server's own `imports`.
- Stub after bootstrap: `vi.spyOn(PlatformTest.get(UsersService), "findById").mockResolvedValue(...)`.

## 6. Use real infrastructure sparingly

Point to `@tsed/testcontainers-mongo` (`https://tsed.dev/tutorials/mongoose.md`) and the premium `@tsedio/testcontainers-*` packages (Postgres, Redis, Vault) listed in `https://tsed.dev/docs/testing.md`. Do not hand-roll container lifecycle code when a package exists.

## Pitfalls

- "Platform type is not specified" (older releases) or a crash while creating the platform adapter in `PlatformTest.bootstrap`: no adapter package was imported. Add `import "@tsed/platform-express";`.
- `beforeEach(PlatformTest.create)` passes Vitest's test context as settings. Write `beforeEach(() => PlatformTest.create())`.
- Missing `PlatformTest.reset` leaks providers, hooks and mocks into the next file and keeps handles open.
- `PlatformTest.invoke` without `await` returns a promise; assertions then run on the promise.
- `PlatformTest.inject([Token], cb)` is deprecated. Use `inject()` or `PlatformTest.invoke()`.
- Services reading `context()` need `runInContext(PlatformTest.createRequestContext(), () => ...)` from `@tsed/di`.
- One `PlatformTest.bootstrap` per file in `beforeAll`, not `beforeEach`: bootstrapping the server per test is slow.
- Do not import from `@tsed/common`; `PlatformTest` lives in `@tsed/platform-http/testing`.

## Checklist

- `vitest.config` uses `unplugin-swc` with `decorators`, `legacyDecorator`, `decoratorMetadata` and `useDefineForClassFields: false`.
- No `new` on a DI-managed class anywhere in the specs; instances come from `inject()`, `PlatformTest.get()` or `PlatformTest.invoke()`.
- Every `PlatformTest.create`/`bootstrap` has a matching `PlatformTest.reset`.
- Mocks use `{token, use}` and are asserted with `toHaveBeenCalledWith`; success and error paths are covered.
- Integration specs import the platform adapter and build requests with `SuperTest(PlatformTest.callback())`.
- Related skills: tsed-di (providers, scopes), tsed-configuration (settings overrides), tsed-controllers, tsed-middlewares, tsed-exceptions (error payload assertions).
