📝

NestJS(TypeScript)でモックをつかったテストコードの書き方

2022/05/02に公開

はじめに

NestJSでモックを使ったテストコードを書く方法を整理します。
テストコード?describe?it何それ?って方は、こちらをご一読ください。
NestJSがよくわからない方は、こちらをご一読ください。

ファイルを作成する

テストしたいサービスクラスのディレクトリにファイルを作成します。
*.service.spec.tsという命名でファイルを作成しましょう。

テストコードを書く

今回、当方が作成したタスク管理アプリ用のバックエンドプログラムを題材にします。
完成ソースコード

モジュールをインポートします。

リポジトリクラスとサービスクラスおよびテストモジュールをインポートします。

tasks.service.spec.ts
import { TasksRepository } from './tasks.repository';
import { TasksService } from './tasks.service';
import { Test } from '@nestjs/testing';

モックをセットアップします。

データベースと接続せずともテストしたいため、モックを作成します。
ここでは、データベースとやりとりするリポジトリクラスのモックを作ります。
今回テストするリポジトリでは、ユーザのデータクラスが必要となるため、ダミーデータも作成します。
リポジトリのモックはjest.fn()としておくことで、ダミーな振る舞いを定義することができます。

tasks.service.spec.ts
const mockTasksRepository = () => ({
  getTasks: jest.fn(), // テストコードで振る舞いを記述するためjest.fn()を利用
});

// リポジトリの引数に必要となるユーザデータクラスのダミーデータ
const mockUser = {
  username: 'someone',
  id: 'test_id',
  password: 'test_password',
  tasks: [],
};

続いて、テストコードを作成します。
まずはbeforeEachメソッド内で、テストモジュールを作成します。
詳細はJESTの公式ドキュメントをご確認ください。

tasks.service.spec.ts
describe('TaskService', () => {
  let tasksService: TasksService;
  let tasksRepository // モックを使用するため型定義しない

  beforeEach(async () => {
    // テストモジュールを作成
    // サービスクラスに対して、リポジトリのモックを注入する
    const module = await Test.createTestingModule({
      providers: [
        TasksService,
        { provide: TasksRepository, useFactory: mockTasksRepository },
      ],
    }).compile();

  // テストモジュールからサービスクラスを受け取る
    tasksService = module.get(TasksService);
   
    // テストモジュールからリポジトリクラスを受け取る
    tasksRepository = module.get(TasksRepository);
  });

 // サービスクラスのgetTasksの呼び出しをテストする
  describe('getTasks', () => {
    it('calls TasksRepository.getTasks', () => {
      // まだ1度も呼ばれていないことをテスト
      expect(tasksRepository.getTasks).not.toHaveBeenCalled();
      // ダミーデータを渡してサービスクラスの処理を呼び出す
      tasksService.getTasks(null, mockUser);
      // 呼び出されたことをテスト
      expect(tasksRepository.getTasks).toHaveBeenCalled();
    });
  });
});

テストはコンソールから

yarn test

と実行します。
テスト結果
image.png
PASSしました。

テストコードを書いて実際にサービスの振る舞いをテストする

では実際にテストコードを書いてみます
先程のdescribe('getTasks')のメソッド内にit句を追加して書いていきます。
このit句内で、リポジトリの振る舞いをユーザ側で定義してあげます。
今回は、getTasksを呼び出すと、someValueという値が返ってくるような振る舞いを定義しました。

tasks.service.spec.ts
    it('calls TasksRepository.getTasks and returns the result', async () => {
      // リポジトリの振る舞いを定義
      tasksRepository.getTasks.mockResolvedValue('someValue');
      // サービスクラスを呼び出す
      const result = await tasksService.getTasks(null, mockUser);
      // resultの中身が一致していることをテスト
      expect(result).toEqual('someValue');
      // resultの中身が一致していないことをテスト
      expect(result).not.toEqual('otherValue');
    });

テスト結果
image.png
テストがPASSしました。

例外のテストコードを書く

続いて、例外のテストコードを書いていきます。
tasks.service.tsの内容を確認しましょう。
getTasksById を呼び出した時に、もしデータが存在しない場合、NotFoundExceptionの例外が発動します。

該当箇所
image.png

まずはリポジトリクラスのモックを追加します。

tasks.service.spec.ts
const mockTasksRepository = () => ({
  getTasks: jest.fn(), 
  findOne: jest.fn(),  // これを追加
});

続いて、describe句とit句を書きます。

tasks.service.spec.ts
  describe('getTaskById', () => {
    it('calls TasksRepository.findOne and returns the result', async () => {
      // テスト用ダミーデータを作成
      const mockTask = {
        title: 'Test title',
        description: 'Test desc',
        id: 'someId',
        status: TaskStatus.OPEN,
      };
      // リポジトリの振る舞いを定義
      tasksRepository.findOne.mockResolvedValue(mockTask);
      // サービスを呼び出す
      const result = await tasksService.getTaskById('someId', mockUser);
      // 想定通りの結果であることを確認
      expect(result).toEqual(mockTask);
    });

    it('calls TasksRepository.findOne and handles an error', async () => {
      // リポジトリの振る舞いを定義
      tasksRepository.findOne.mockResolvedValue(null);
      // 想定の例外が発生することを確認
      expect(tasksService.getTaskById('someId', mockUser)).rejects.toThrow(
        NotFoundException,
      );
    });
  });

テスト結果
image.png
無事にパスしました。

終わりに

NestJSにおけるモックを利用したテストについて解説しました。
サービスクラスの振る舞いが、想定通りの結果になっているか、例外処理も含めてテストする方法について整理しました。

以上です。

Discussion