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.
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
orPlatformTest.invoke
, - Reset the context with
PlatformTest.reset
.
Here is an example to test the ParseService:
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");
});
});
});
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");
});
});
});
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
):
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");
});
});
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:
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);
}
}
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:
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.
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");
});
});
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.
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");
});
});
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:
npm install --save-dev supertest @types/supertest
yarn add -D supertest @types/supertest
pnpm add -D supertest @types/supertest
bun add -D supertest @types/supertest
Example
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");
});
});
});
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:
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");
});
});
});
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:
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");
});
});
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.
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");
});
});
});
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:
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()
:
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);
});