GAミント至上主義

Web Monomaniacal Developer.

NestJSのTest.createTestingModuleの基本的な使い方。ServiceのMock化とかforRoot系の指定とか

NestJSの新規開発で、テストを有効活用しながら開発を進めてるけどTest.createTestingModuleでいろいろハマったのでポイントをメモ。 使い慣れてくると超便利。

今作ってるe2eテストの場合、createTestingModuleはこんな感じ。コメントでメモ。 beforeAll, もしくはbeforeEachでテスト用のNestアプリケーションをつくってそれぞれのケースで使うことになる。

import { Repository } from 'typeorm'
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import { getRepositoryToken } from '@nestjs/typeorm'
import { SendGridService } from '@anchan828/nest-sendgrid'
// 自分で作ったモック
import { SendGridServiceMock } from './test.lib'

describe('User Authorization (e2e)', () => {
  // 複数のメソッド内で使うやつはここらへんでletしておく
  let app: INestApplication
  let token: string
  let userRepository: Repository<User>
  let usersService: UsersService
  let userEmailVerificationRepository: Repository<UserEmailVerification>

  beforeAll(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [],
      imports: [
        UsersModule,
        // テストではこのモジュールがRootになるのでHogeModule.forRoot系はだいたい必要になる
        // 接続名にtestのものを使用するようにしてる、いろんなテストで使うので実際は共通化してimportして使ってる。
        TypeOrmModule.forRootAsync({
          useFactory: async () => {
            return {
              ...(await getConnectionOptions('test')),
              // nameが残ってるとtestConnectionを生成してしまってエラーになるので消してる
              name: null,
            }
          },
        })
        // Entityに紐付いたRepositoryを書き出してくれる。Moduleでやってるときは不要。カスタムレポジトリの場合も別
        TypeOrmModule.forFeature([User, UserProfile, /* 省略*/]),
      ],
    })
    // メール送信に@anchan828/nest-sendgridを使用したけどテスト中は送られると困るのでMockに差し替える 
      .overrideProvider(SendGridService)
      .useClass(SendGridServiceMock)
      .compile()
    app = module.createNestApplication()
    // テスト中に使いたいサービス、レポジトリなどはこの形で抜き出せる
    userEmailVerificationRepository = app.get<
      Repository<UserEmailVerification>
    >(getRepositoryToken(UserEmailVerification))
    userRepository = app.get<Repository<User>>(getRepositoryToken(User))
    usersService = app.get<UsersService>(UsersService)
    // モックのメソッドが呼ばれたか判定したいのでspyを仕込んでおく
 // expect(sendGridService.send).toHaveBeenCalled() のような感じでチェックできる
    sendGridService = app.get<SendGridService>(SendGridService)
    jest.spyOn(sendGridService, 'send')
    // バリデーションなどのPipeも設定が必要
    app.useGlobalPipes(
      new ValidationPipe({
        transform: true,
      }),
    )

imports

そのテストで使うModuleを指定する。自分で作成したModuleの他、 あとテスト中はこのモジュールがRootになるのでTypeOrmModule.forRootAsync()(もしくはforRoot())や、 よく使うConfigModule、EventEmitterModuleなどもそれぞれforRoot()があるので必要な場合は追加する。 テストファイルが増えてくるとだいたい同じのを何度も書くことになるのでサービス単位などで共通化しておくと良さそう。

providers

サービスなどの@Injectable()なClassを指定するけど、基本的に上記のModuleで指定しているものが多いと思うので、サービスの単体テスト等でなければ指定する機会がないかも。 importのModule内と、ここで2重で読み込んだりするとなかなか分かりにくいエラーが出たりするので注意。

サービスなどのMock化

この箇所で行っている。

      .overrideProvider(SendGridService)
      .useClass(SendGridServiceMock)

元のサービスの型をみて今回はこんな感じのにした。送らないけど中身もちょっと目視したいので一旦console.logで出してる。

export class SendGridServiceMock {
  send(data: Partial<MailDataRequired>) {
    console.log(
      `SendGridServiceMock.send() from:${data.from} to:${data.to} subject:${data.subject}`,
    )
  }
  sendMultiple(data: Partial<MailDataRequired>) {
    console.log(
      `SendGridServiceMock.sendMultiple() from:${data.from} to:${data.to} subject:${data.subject}`,
    )
  }
}