NestJSでUnitTest書いてみた

NestJSでユニットテスト書いてみました。
モックが作りやすいのが特徴だと思います。

プロジェクトを作るところから実装していこうと思いますので、必要に応じて読み飛ばしながらお読みください🙇

とりあえずプロジェクトつくってみます

以下のコマンドでNestJSのプロジェクトを作ります。

% nest new my-app

nestコマンドがない方はあらかじめ以下のコマンドでnestをインストールしておきます。

% yarn global add @nestjs/cli

npm派の人はnpmに読み替えてください。

とりあえずテストを実行してみる

デフォルトの状態でテストが実行できるようです。
yarn testを実行してみます。

% yarn test
yarn run v1.22.19
$ jest
 PASS  src/app.controller.spec.ts
  AppController
    root
      ✓ should return "Hello World!" (12 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.666 s
Ran all test suites.
✨  Done in 5.53s.

何かのテストが実行されました。

実行されたのはsrc/app.controller.spec.tsのコードのようです。

import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let appController: AppController; // モック対象

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [AppController],
      providers: [AppService],
    }).compile();

    appController = app.get<AppController>(AppController);  // モックを代入
  });

  describe('root', () => {
    it('should return "Hello World!"', () => {
      expect(appController.getHello()).toBe('Hello World!');
    });
  });
});

上記を少し解説すると、appControllerのテストをするためにappControllerをモック化しています。
すると、当然appControllerのテストは実行されずにモックで設定した値がテストされます。

ちょっと何を言っているかわからないかもしれませんが、簡単に言うと「appControllerのテストができてない」ということです。

デフォルトのテストコードはあまりイケてないです。

あるべきテストに修正します。

あるべきテストとは?

まず、あるべきテストを考える前に、NestJSの構成を説明します。

以下の3つの基本要素からできているのがNestJSです。

  • module・・・依存関係を解決する。ロジックは書かない。
  • controller ・・・HTTPリクエストとレスポンスを処理する。いわゆるプレゼンテーション層。
  • service・・・ビジネスロジックを書く。いわゆるユースケース層です。

ですので、contorollerのテストを行うときはserviceをモック化する必要があります。

※ 補足
 例えば、service層の先にデータベースに接続するクラスを作ったのであれば、データベースに接続するクラスをモック化すると良いです。
 標準でDIを提供しているので、簡単にどんなクラスでもモック化できます。

では実際にテストを修正していきます。

まずはテスト対象をおさらいします

テスト対象はapp.controller.tsです。

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello(); // ここをモック化するべき
  }
}

serviceであるappServiceをモックにすることでAppControllerに対して高いカバレッジのテストを実行することができます。

テストを修正していきます。

先ほどのapp.controller.spec.tsを開きます。
まずは宣言を変更します。

// let appController: AppController;
let appService: AppService; // serviceに変更

importがない、と怒られるので以下を追加します。

import { AppService } from './app.service';

次にMockを変更します。

  beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      // controllers: [AppController], いらないのでコメントアウト
      providers: [
        {
          provide: AppService,
          useValue: {
            getHello: jest.fn().mockReturnValue('MOCK: Hello World!'), // Mockを追加
          },
        },
      ],
    }).compile();

    // appController = app.get<AppController>(AppController); いらないのでコメントアウト
    appService = app.get<AppService>(AppService); // Serviceに変更
  });

    // appController = app.get<AppController>(AppController); いらないのでコメントアウト
    appService = app.get<AppService>(AppService); // Serviceに変更
  });

次にアサーションを書いていきます。


 // 古いコードはコメントアウト 
 // describe('root', () => {
  //   it('should return "Hello World!"', () => {
  //     expect(appService.getHello()).toBe('Hello World!');
  //   });
  // });

  describe('Hello World', () => {
    it('should return "Hello World!"', () => {
      const controller = new AppController(appService); // ★ポイント
      expect(controller.getHello()).toBe('MOCK: Hello World!');
    });
  });

モック化したserviceをcontrollerに渡すのがポイントです。

完成したテスト

削除したコードを以下に貼ります。
不要なコメントは消してあります。

実行結果

実行結果は以下の通りです。

% yarn test
yarn run v1.22.19
$ jest
 PASS  src/app.controller.spec.ts
  AppController
    Hello World
      ✓ should return "Hello World!" (12 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.875 s, estimated 4 s
Ran all test suites.
✨  Done in 5.07s.

感想など

モックが簡単に書けるのが魅力でした。
また、今の時代、テストコードはChatGPTで作成できるので、AIの力を借りつつ半自動生成でテストできるので生産性も高い印象です。

反面、実際にデータベースに接続するテストのセットアップが難しくて断念しています。
また、インテグレーションテストも上手く設定ができなくて困っています。

まだ情報が少ない言語なので苦労することは多いです。