NestJS は Jest でコードを書いてテストを行うらしい。
Jest ではモックを使うことができる。Controllerをテストする際は、Serviceをモックに置き換えて単体テストし、Serviceをテストする際は、Repositoryをモックに置き換えて単体テストするようだ。
Jest におけるモックの作り方は以下が参考になる。
例
以下のように Controller が Service に依存していて、Service が TypeORM を使った Repository に依存している場合を考える。
Controller をテストする
UsersController の単体テストをする場合、UsersController が依存している UsersService をモックに差し替えてテストすることになる。
NestJS のテストコードではテスト対象のクラスと、テスト対象のクラスが依存しているクラスを定義したモジュールというものを作成する必要がある。
UsersService のモックを作る
import { HttpException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { User } from "./entities/user.entity";
import { UserNotFoundError } from "./errors/user-not-found-error";
import { UsersController } from "./users.controller";
import { UsersService } from "./users.service";
// UsersService の持つメソッドを羅列する。
const mockUsersService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
describe("UsersController", () => {
let controller: UsersController;
// 各it()を実行する前に実行される関数
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [
UsersController, // テスト対象
],
providers: [
// UsersServiceをmockUsersServiceに置き換える
{ provide: UsersService, useValue: mockUsersService },
],
}).compile();
controller = module.get<UsersController>(UsersController);
// spyOn()をクリアする
jest.resetAllMocks();
});
// 以下にit()でテストコードを書く
});
単純なモックメソッドを作成する
// 略
describe("UsersController", () => {
// 略
it("create()のテスト (正常)", async () => {
const user: User = {
id: "exampleId",
name: "exampleUser",
};
// モックのcreate()の返り値を設定する
const spy = jest.spyOn(mockUsersService, "create").mockResolvedValue(user);
// テスト対象のメソッドをコールして、戻り値を確認する
expect(await controller.create({ name: "exampleUser" })).toEqual(user);
// モックのメソッドが呼ばれたか確認する
expect(spy).toHaveBeenCalled();
});
});
例外を確認する
// 略
describe("UsersController", () => {
// 略
it("create()のテスト (データベースが異常)", async () => {
// モックのメソッド内で例外を返す
const spy = jest
.spyOn(mockUsersService, "create")
.mockRejectedValue(new Error());
// awaitはつけずrejects.toThrow()でチェックする
expect(controller.create({ name: "exampleUser" })).rejects.toThrow(
new HttpException("Internal server error", 500)
);
expect(spy).toHaveBeenCalled();
});
});
Service をテストする
UsersService の単体テストをする場合、UsersService が参照している Repository をモックに差し替えてテストすることになる。(UsersService はテスト前でバグがあるかもしれないコードであるから、本番環境のデータベースに接続したくないはずだ)
やり方は Controller と同じ。
import { Test, TestingModule } from "@nestjs/testing";
import { getRepositoryToken } from "@nestjs/typeorm";
import { User } from "./entities/user.entity";
import { UserNotFoundError } from "./errors/user-not-found-error";
import { UsersService } from "./users.service";
const mockUsersRepository = {
save: jest.fn(),
find: jest.fn(),
findAll: jest.fn(),
findOneBy: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
};
describe("UsersService", () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService, // テスト対象のService
{ provide: getRepositoryToken(User), useValue: mockUsersRepository },
],
}).compile();
service = module.get<UsersService>(UsersService);
jest.resetAllMocks();
});
it("create()のテスト", async () => {
const spy = jest.spyOn(mockUsersRepository, "save");
// テスト対象のメソッドをコールする
const result = await service.create({ name: "exampleUser" });
// Repositoryをコールしているかを確認する
expect(spy).toHaveBeenCalled();
// 戻り値チェック
expect(result.id).toBeDefined();
expect(result.name).toBe("exampleUser");
});
it("findAll()のテスト", async () => {
const spy = jest.spyOn(mockUsersRepository, "find").mockResolvedValue([]);
// テスト対象のメソッドをコールする
const result = await service.findAll();
// Repositoryをコールしているかを確認する
expect(spy).toHaveBeenCalled();
// 戻り値チェック
expect(result).toEqual([]);
spy.mockRestore();
});
it("findOne()のテスト (正常)", async () => {
const user: User = {
id: "54b4b67f-f84f-4371-8928-089803113201",
name: "exampleUser",
};
const spy = jest
.spyOn(mockUsersRepository, "findOneBy")
.mockResolvedValue(user);
// テスト対象のメソッドをコールする
const result = await service.findOne(user.id);
// Repositoryをコールしているかを確認する
expect(spy).toHaveBeenCalled();
// 戻り値チェック
expect(result).toEqual(user);
});
it("findOne()のテスト (ユーザーIDが存在しない)", async () => {
const spy = jest
.spyOn(mockUsersRepository, "findOneBy")
.mockRejectedValue(new UserNotFoundError());
expect(service.findOne("exampleId")).rejects.toThrow(
new UserNotFoundError()
);
expect(spy).toHaveBeenCalled();
});
it("update()のテスト (正常)", async () => {
const user: User = {
id: "54b4b67f-f84f-4371-8928-089803113201",
name: "exampleUser",
};
const spyFindOneBy = jest
.spyOn(mockUsersRepository, "findOneBy")
.mockResolvedValue(user);
const spyUpdate = jest.spyOn(mockUsersRepository, "update");
await service.update(user.id, { name: user.name });
expect(spyFindOneBy).toHaveBeenCalled();
expect(spyUpdate).toHaveBeenCalled();
});
it("update()のテスト (ユーザーIDが存在しない)", async () => {
const spy = jest
.spyOn(mockUsersRepository, "findOneBy")
.mockRejectedValue(new UserNotFoundError());
expect(
service.update("exampleId", { name: "exampleName" })
).rejects.toThrow(new UserNotFoundError());
expect(spy).toHaveBeenCalled();
});
it("remove()のテスト (正常)", async () => {
const user: User = {
id: "54b4b67f-f84f-4371-8928-089803113201",
name: "exampleUser",
};
const spyFindOneBy = jest
.spyOn(mockUsersRepository, "findOneBy")
.mockResolvedValue(user);
const spyDelete = jest.spyOn(mockUsersRepository, "delete");
await service.remove(user.id);
expect(spyFindOneBy).toHaveBeenCalled();
expect(spyDelete).toHaveBeenCalled();
});
it("remove()のテスト (ユーザーIDが存在しない)", async () => {
const spy = jest
.spyOn(mockUsersRepository, "findOneBy")
.mockRejectedValue(new UserNotFoundError());
expect(service.remove("exampleId")).rejects.toThrow(
new UserNotFoundError()
);
expect(spy).toHaveBeenCalled();
});
});
テストが動かない
constructor で async 関数を動かすとテストが動いたり動かなかったりした。テストのセットアップ中に非同期処理が動くせいでうまくいかない可能性がある。
constructor で async 関数を動かすのを止めると解決した。
テストコードの src から始まる import が通らない
$ npx jest ./src/todos/todos.service.spec.ts
FAIL src/todos/todos.service.spec.ts
● Test suite failed to run
Cannot find module 'src/users/errors/user-not-found-error' from 'todos/todos.service.spec.ts'
2 | import { CreateUserDto } from 'src/users/dto/create-user.dto';
3 | import { User } from 'src/users/entities/user.entity';
> 4 | import { UserNotFoundError } from 'src/users/errors/user-not-found-error';
| ^
5 | import { IUsersService } from 'src/users/users.service';
6 | import { TodosService } from './todos.service';
7 |
at Resolver._throwModNotFoundError (../node_modules/jest-resolve/build/resolver.js:427:11)
at Object.<anonymous> (todos/todos.service.spec.ts:4:1)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 1.32 s
Ran all test suites matching /.\/src\/todos\/todos.service.spec.ts/i.
package.jsonに以下をマージする。
{
"jest": {
"moduleNameMapper": {
"src(.*)$": "<rootDir>/$1"
}
}
}
カバレッジを確認する
$ npm run test:cov # npx jest --coverageと同義
# 略
--------------------------|---------|----------|---------|---------|----------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------|---------|----------|---------|---------|----------------------------
All files | 61.08 | 57.14 | 57.57 | 59.79 |
src | 32.5 | 0 | 50 | 26.47 |
app.controller.ts | 100 | 100 | 100 | 100 |
app.module.ts | 0 | 0 | 0 | 0 | 1-39
app.service.ts | 100 | 100 | 100 | 100 |
main.ts | 0 | 100 | 0 | 0 | 1-11
src/todos | 37.8 | 37.5 | 21.42 | 35.52 |
todos.controller.ts | 57.89 | 60 | 42.85 | 55.55 | 35-41,47-50,59-63,70-73,79
todos.module.ts | 0 | 100 | 100 | 0 | 1-13
todos.service.ts | 25.71 | 0 | 0 | 21.21 | 14-78
src/todos/dto | 100 | 100 | 100 | 100 |
create-todo.dto.ts | 100 | 100 | 100 | 100 |
update-todo.dto.ts | 100 | 100 | 100 | 100 |
src/todos/entities | 100 | 100 | 100 | 100 |
todo.entity.ts | 100 | 100 | 100 | 100 |
src/todos/errors | 100 | 100 | 100 | 100 |
todo-not-found-error.ts | 100 | 100 | 100 | 100 |
src/users | 87.69 | 100 | 100 | 89.83 |
users.controller.ts | 100 | 100 | 100 | 100 |
users.module.ts | 0 | 100 | 100 | 0 | 1-13
users.service.ts | 100 | 100 | 100 | 100 |
src/users/dto | 100 | 100 | 100 | 100 |
create-user.dto.ts | 100 | 100 | 100 | 100 |
update-user.dto.ts | 100 | 100 | 100 | 100 |
src/users/entities | 100 | 100 | 100 | 100 |
user.entity.ts | 100 | 100 | 100 | 100 |
src/users/errors | 100 | 100 | 100 | 100 |
user-not-found-error.ts | 100 | 100 | 100 | 100 |
--------------------------|---------|----------|---------|---------|----------------------------
コマンド実行後coverageディレクトリが生成される。HTML から棒グラフでカバレッジを確認する事もできる。