Skip to content

Testing

Unit test

Installation

Ts.ED support officially two unit test frameworks: Jest, Mocha and Vitest. It's also possible to use your preferred frameworks. Your feedback are welcome.

  • Installation guide for Jest
  • Installation guide for Vitest

Usage

Ts.ED provides PlatformTest to create a new context to inject your Services, Controllers, Middlewares, etc... registered with annotations like Injectable.

The process to test any components is the same thing:

  • Create a new context for your unit test with PlatformTest.create,
  • Inject or invoke your component with PlatformTest.inject or PlatformTest.invoke,
  • Reset the context with PlatformTest.reset.

Here is an example to test the ParseService:

ts
import {it, expect, describe, beforeEach, afterEach} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import {ParseService} from "./ParseService";

describe("ParseService", () => {
  beforeEach(PlatformTest.create);
  afterEach(PlatformTest.reset);
  describe("eval()", () => {
    it("should evaluate expression with a scope and return value", () => {
      const service = PlatformTest.get<ParseService>(ParseService);

      expect(
        service.eval("test", {
          test: "yes"
        })
      ).toEqual("yes");
    });
  });
});
ts
import {it, expect, describe, beforeEach, afterEach} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import {ParseService} from "./ParseService.js";

describe("ParseService", () => {
  beforeEach(PlatformTest.create);
  afterEach(PlatformTest.reset);
  describe("eval()", () => {
    it("should evaluate expression with a scope and return value", () => {
      const service = PlatformTest.get<ParseService>(ParseService);

      expect(
        service.eval("test", {
          test: "yes"
        })
      ).toEqual("yes");
    });
  });
});
ts
import {getValue, isEmpty} from "@tsed/core";
import {Injectable} from "@tsed/di";

@Injectable()
export class ParseService {
  static clone = (src: any): any => JSON.parse(JSON.stringify(src));

  eval(expression: string, scope: any, clone: boolean = true): any {
    if (isEmpty(expression)) {
      return typeof scope === "object" && clone ? ParseService.clone(scope) : scope;
    }

    const value = getValue(scope, expression);

    return typeof value === "object" && clone ? ParseService.clone(value) : value;
  }
}

Async / Await

Testing asynchronous method is also possible using Promises (async/await):

ts
import {PlatformTest} from "@tsed/platform-http/testing";
import {DbService} from "../services/db";

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

  it("should data from db", async () => {
    const dbService = PlatformTest.get<DbService>(DbService);
    const result = await dbService.getData();
    expect(typeof result).toEqual("object");
  });
});
ts
import {PlatformTest} from "@tsed/platform-http/testing";
import {DbService} from "../services/db";

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

  it("should data from db", async () => {
    const dbService = PlatformTest.get<DbService>(DbService);
    const result = await dbService.getData();
    expect(typeof result).toEqual("object");
  });
});

Mock context

Context is a feature that allows you to store data in a global context during the request lifecycle.

Here is an example of context usage:

ts
import {Injectable, Controller, InjectContext} from "@tsed/di";
import {PlatformContext} from "@tsed/platform-http";

@Injectable()
export class CustomRepository {
  @InjectContext()
  protected $ctx?: PlatformContext;

  async findById(id: string) {
    this.ctx?.logger.info("Where are in the repository");

    return {
      id,
      headers: this.$ctx?.request.headers
    };
  }
}

@Controller("/async-hooks")
export class AsyncHookCtrl {
  @Inject()
  private readonly repository: CustomRepository;

  @Get("/:id")
  async get(@PathParams("id") id: string) {
    return this.repository.findById(id);
  }
}
ts
import {context, controller, injectable} from "@tsed/di";
import {PlatformContext} from "@tsed/platform-http";
import {PathParams} from "@tsed/platform-params";
import {Get} from "@tsed/schema";

export class CustomRepository {
  async findById(id: string) {
    const $ctx = context<PlatformContext>();
    $ctx?.logger.info("Where are in the repository");

    return {
      id,
      headers: $ctx?.request.headers
    };
  }
}

injectable(CustomRepository);

export class AsyncHookCtrl {
  private readonly repository = inject(CustomRepository);

  @Get("/:id")
  async get(@PathParams("id") id: string) {
    return this.repository.findById(id);
  }
}

controller(AsyncHookCtrl).path("/async-hooks");

To run a method with context in your unit test, you can use the runInContext function:

ts
import {inject, runInContext} from "@tsed/di";
import {PlatformTest} from "@tsed/platform-http/testing";
import {CustomRepository} from "./CustomRepository";

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

  it("should run method with the ctx", async () => {
    const ctx = PlatformTest.createRequestContext();
    const service = inject(CustomRepository);

    ctx.request.headers = {
      "x-api": "api"
    };

    const result = await runInContext(ctx, () => service.findById("id"));

    expect(result).toEqual({
      id: "id",
      headers: {
        "x-api": "api"
      }
    });
  });
});

Mock dependencies

Using PlatformTest.invoke

PlatformTest API provides an PlatformTest.invoke method to create a new instance of your component with mocked dependencies during a test context created with PlatformTest.create(). This method is useful when you want to mock dependencies for a specific test.

ts
import {PlatformTest} from "@tsed/platform-http/testing";
import {MyCtrl} from "../controllers/MyCtrl";
import {DbService} from "../services/DbService";

describe("MyCtrl", () => {
  // bootstrap your Server to load all endpoints before run your test
  beforeEach(PlatformTest.create);
  afterEach(PlatformTest.reset);

  it("should do something", async () => {
    const locals = [
      {
        token: DbService,
        use: {
          getData: () => {
            return "test";
          }
        }
      }
    ];

    // give the locals map to the invoke method
    const instance: MyCtrl = await PlatformTest.invoke(MyCtrl, locals);

    // and test it
    expect(!!instance).toEqual(true);
    expect(instance.getData()).toEqual("test");
  });
});
ts
import {it, expect, describe, beforeEach, afterEach} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import {MyCtrl} from "../controllers/MyCtrl";
import {DbService} from "../services/DbService";

describe("MyCtrl", () => {
  // bootstrap your Server to load all endpoints before run your test
  beforeEach(PlatformTest.create);
  afterEach(PlatformTest.reset);

  it("should do something", async () => {
    const locals = [
      {
        token: DbService,
        use: {
          getData: () => {
            return "test";
          }
        }
      }
    ];

    // give the locals map to the invoke method
    const instance: MyCtrl = await PlatformTest.invoke(MyCtrl, locals);

    // and test it
    expect(!!instance).toEqual(true);
    expect(instance.getData()).toEqual("test");
  });
});

TIP

PlatformTest.invoke() executes automatically the $onInit hook!

Using PlatformTest.create

If you want to mock dependencies for all your tests, you can use the PlatformTest.create() method. it useful if you have a service that execute a code in his constructor.

ts
import {PlatformTest} from "@tsed/platform-http/testing";
import {MyCtrl} from "../controllers/MyCtrl.js";
import {DbService} from "../services/DbService.js";

describe("MyCtrl", () => {
  // bootstrap your Server to load all endpoints before run your test
  beforeEach(() =>
    PlatformTest.create({
      imports: [
        {
          token: DbService,
          use: {
            getData: () => {
              return "test";
            }
          }
        }
      ]
    })
  );
  afterEach(PlatformTest.reset);

  it("should do something", () => {
    const instance: MyCtrl = PlatformTest.get(MyCtrl);

    // and test it
    expect(!!instance).toEqual(true);
    expect(instance.getData()).toEqual("test");
  });
});
ts
import {it, expect, describe, beforeEach, afterEach} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import {MyCtrl} from "../controllers/MyCtrl.js";
import {DbService} from "../services/DbService.js";

describe("MyCtrl", () => {
  // bootstrap your Server to load all endpoints before run your test
  beforeEach(() =>
    PlatformTest.create({
      imports: [
        {
          token: DbService,
          use: {
            getData: () => {
              return "test";
            }
          }
        }
      ]
    })
  );
  afterEach(PlatformTest.reset);

  it("should do something", () => {
    const instance: MyCtrl = PlatformTest.get(MyCtrl);

    // and test it
    expect(!!instance).toEqual(true);
    expect(instance.getData()).toEqual("test");
  });
});

Test your Rest API

Installation

To test your API, I recommend you to use the supertest module.

To install supertest just run these commands:

sh
npm install --save-dev supertest @types/supertest
sh
yarn add -D supertest @types/supertest
sh
pnpm add -D supertest @types/supertest
sh
bun add -D supertest @types/supertest

Example

ts
import {PlatformTest} from "@tsed/platform-http/testing";
import * as SuperTest from "supertest";
import {Server} from "../Server.js";

describe("Rest", () => {
  beforeAll(PlatformTest.bootstrap(Server));
  afterAll(PlatformTest.reset);

  describe("GET /rest/calendars", () => {
    it("should do something", async () => {
      const request = SuperTest(PlatformTest.callback());
      const response = await request.get("/rest/calendars").expect(200);

      expect(typeof response.body).toEqual("array");
    });
  });
});
ts
import {it, expect, describe, beforeAll, afterAll} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import * as SuperTest from "supertest";
import {Server} from "../Server.js";

describe("Rest", () => {
  beforeAll(PlatformTest.bootstrap(Server));
  afterAll(PlatformTest.reset);

  describe("GET /rest/calendars", () => {
    it("should do something", async () => {
      const request = SuperTest(PlatformTest.callback());
      const response = await request.get("/rest/calendars").expect(200);

      expect(typeof response.body).toEqual("array");
    });
  });
});

WARNING

If you use the PlatformTest, you'll probably get an error when you'll run the unit test:

Platform type is not specified. Have you added at least `import @tsed/platform-express` (or equivalent) on your Server.ts ?

To solve it, just add the import @tsed/platform-express on your Server.ts. PlatformTest need this import to know on which Platform your server must be executed for integration test.

Pros / Cons

WARNING

Use PlatformTest.boostrap() is not recommended in Jest environment.
This method is practical for carrying out some integration tests but consumes a lot of resources which can lead to a significant slowdown in your tests or even cause timeouts.

It's better to write your tests using Cucumber and test your Rest applications in a container.

Note

There is no performance issue as long as you use PlatformTest.create() to perform your tests, But it's not possible with this method to do an integration test with the server (Express or Koa). You can only test your controller and the services injected into it.

Stub a service method

When you're testing your API, you have sometimes to stub a method of a service.

Here is an example to do that:

typescript
import {PlatformTest} from "@tsed/platform-http/testing";
import SuperTest from "supertest";
import {Server} from "../../Server";
import {Chapter} from "../../entity/Chapter";

const entity = new Chapter();
Object.assign(entity, {
  id: 2,
  bookId: 4,
  timestamp: 1650996201,
  name: "First Day At Work"
});

describe("ChapterController", () => {
  beforeAll(PlatformTest.bootstrap(Server));
  afterAll(PlatformTest.reset);

  describe("GET /rest/chapter", () => {
    it("Get All Chapters", async () => {
      const service = PlatformTest.get(ChapterService);

      jest.spyOn(service, "findChapters").mockResolvedValue([entity]);

      const request = SuperTest(PlatformTest.callback());

      const response = await request.get("/rest/chapter").expect(200);
      expect(typeof response.body).toEqual("object");
    });
  });
});
typescript
import {it, expect, describe, beforeAll, afterAll} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import SuperTest from "supertest";
import {Server} from "../../Server.js";
import {Chapter} from "../../entity/Chapter.js";

const entity = new Chapter();
Object.assign(entity, {
  id: 2,
  bookId: 4,
  timestamp: 1650996201,
  name: "First Day At Work"
});

describe("ChapterController", () => {
  beforeAll(PlatformTest.bootstrap(Server));
  afterAll(PlatformTest.reset);

  describe("GET /rest/chapter", () => {
    it("Get All Chapters", async () => {
      const service = PlatformTest.get(ChapterService);

      jest.spyOn(service, "findChapters").mockResolvedValue([entity]);

      const request = SuperTest(PlatformTest.callback());

      const response = await request.get("/rest/chapter").expect(200);
      expect(typeof response.body).toEqual("object");
    });
  });
});

Stub a middleware method 6.114.3+

When you're testing your API, you have sometimes to stub middleware to disable authentication for example.

Here is an example to do that:

typescript
import {PlatformTest} from "@tsed/platform-http/testing";
import SuperTest from "supertest";
import {TestMongooseContext} from "@tsed/testing-mongoose";
import {HelloWorldController} from "./HelloWorldController.js";
import {Server} from "../../Server.js";
import {AuthMiddleware} from "../../middlewares/auth.middleware.js";

describe("HelloWorldController", () => {
  beforeAll(async () => {
    await TestMongooseContext.bootstrap(Server)();

    const authMiddleware = PlatformTest.get<AuthMiddleware>(AuthMiddleware);
    jest.spyOn(authMiddleware, "use").mockResolvedValue(true);
  });
  beforeEach(() => {
    jest.clearAllMocks();
  });
  afterAll(TestMongooseContext.reset);

  it("should return value", async () => {
    const request = SuperTest(PlatformTest.callback());
    const response = await request.get("/rest/hello-world").expect(200);
    expect(response.text).toEqual("hello");
  });
});
typescript
import {it, expect, describe, beforeAll, afterAll, beforeEach} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import SuperTest from "supertest";
import {TestMongooseContext} from "@tsed/testing-mongoose";
import {HelloWorldController} from "./HelloWorldController.js";
import {Server} from "../../Server.js";
import {AuthMiddleware} from "../../middlewares/auth.middleware.js";

describe("HelloWorldController", () => {
  beforeAll(async () => {
    await TestMongooseContext.bootstrap(Server)();

    const authMiddleware = PlatformTest.get<AuthMiddleware>(AuthMiddleware);
    jest.spyOn(authMiddleware, "use").mockResolvedValue(true);
  });
  beforeEach(() => {
    jest.clearAllMocks();
  });
  afterAll(TestMongooseContext.reset);

  it("should return value", async () => {
    const request = SuperTest(PlatformTest.callback());
    const response = await request.get("/rest/hello-world").expect(200);
    expect(response.text).toEqual("hello");
  });
});

Testing session

To install session with Ts.ED see our documentation page.

ts
import {PlatformTest} from "@tsed/platform-http/testing";
import * as SuperTest from "supertest";
import {Server} from "../../../src/Server.js";

describe("Session", () => {
  beforeAll(PlatformTest.bootstrap(Server));
  afterAll(PlatformTest.reset);

  describe("Login / Logout", () => {
    it("should create session return hello world and connect a fake user", async () => {
      const request = SuperTest.agent(PlatformTest.callback());
      // WHEN
      const response1 = await request.get("/rest/whoami").expect(200);

      await request.post("/rest/login").send({name: "UserName"}).expect(204);

      const response2 = await request.get("/rest/whoami").expect(200);

      await request.post("/rest/logout").expect(204);

      const response3 = await request.get("/rest/whoami").expect(200);

      // THEN
      expect(response1.text).toEqual("Hello world");
      expect(response2.text).toEqual("Hello user UserName");
      expect(response3.text).toEqual("Hello world");
    });
  });
});
ts
import {it, expect, describe, beforeAll, afterAll} from "vitest";
import {PlatformTest} from "@tsed/platform-http/testing";
import * as SuperTest from "supertest";
import {Server} from "../../../src/Server.js";

describe("Session", () => {
  let request: any;

  beforeAll(() => {
    PlatformTest.bootstrap(Server);
    request = SuperTest.agent(PlatformTest.callback());
  });
  afterAll(PlatformTest.reset);

  describe("Login / Logout", () => {
    it("should create session return hello world and connect a fake user", async () => {
      const request = SuperTest.agent(PlatformTest.callback());
      // WHEN
      const response1 = await request.get("/rest/whoami").expect(200);

      await request.post("/rest/login").send({name: "UserName"}).expect(204);

      const response2 = await request.get("/rest/whoami").expect(200);

      await request.post("/rest/logout").expect(204);

      const response3 = await request.get("/rest/whoami").expect(200);

      // THEN
      expect(response1.text).toEqual("Hello world");
      expect(response2.text).toEqual("Hello user UserName");
      expect(response3.text).toEqual("Hello world");
    });
  });
});

Testing with mocked service v7.4.0

One inconvenient with PlatformTest.bootstrap() and PlatformTest.create() is that they will always call the hooks of your service like for example $onInit().

Note

PlatformTest.create() call only the $onInit() hook while PlatformTest.bootstrap() call all hooks.

This is going to be a problem when you want to test your application, and it uses $onInit to initialize your database or something else.

Since v7.4.0, You can now mock one or more services as soon as the PlatformTest context is created (like is possible with PlatformTest.invoke).

Here is an example:

typescript
import {MyCtrl} from "../controllers/MyCtrl";
import {DbService} from "../services/DbService";

describe("MyCtrl", () => {
  // bootstrap your Server to load all endpoints before run your test
  beforeEach(() =>
    PlatformTest.create({
      imports: [
        {
          token: DbService,
          use: {
            getData: () => {
              return "test";
            }
          }
        }
      ]
    })
  );
  afterEach(() => PlatformTest.reset());
});

It's also possible to do that with PlatformTest.bootstrap():

typescript
import {PlatformTest} from "@tsed/platform-http/testing";
import SuperTest from "supertest";
import {Server} from "../../Server";

describe("SomeIntegrationTestWithDB", () => {
  beforeAll(
    PlatformTest.bootstrap(Server, {
      imports: [
        {
          token: DbService,
          use: {
            getData: () => {
              return "test";
            }
          }
        }
      ]
    })
  );
  afterAll(PlatformTest.reset);
});

Released under the MIT License.