0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NestJS でテストを行う

Posted at

NestJS は Jest でコードを書いてテストを行うらしい。

Jest ではモックを使うことができる。Controllerをテストする際は、Serviceをモックに置き換えて単体テストし、Serviceをテストする際は、Repositoryをモックに置き換えて単体テストするようだ。

Jest におけるモックの作り方は以下が参考になる。

以下のように Controller が Service に依存していて、Service が TypeORM を使った Repository に依存している場合を考える。

Controller をテストする

UsersController の単体テストをする場合、UsersController が依存している UsersService をモックに差し替えてテストすることになる。

NestJS のテストコードではテスト対象のクラスと、テスト対象のクラスが依存しているクラスを定義したモジュールというものを作成する必要がある。

UsersService のモックを作る

src/users/users.controller.spec.ts
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()でテストコードを書く
});

単純なモックメソッドを作成する

src/users/users.controller.spec.ts
// 略
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();
  });
});

例外を確認する

src/users/users.controller.spec.ts
// 略
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 と同じ。

src/users/users.service.spec.ts
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に以下をマージする。

json title="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 から棒グラフでカバレッジを確認する事もできる。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?