Oteto Blogのロゴ

NestJS(Fastify)+ JestでE2Eテストを実装する

NestJS+ Jestで最低限のE2Eテストを実装する(HTTPアダプターとしてFastifyを使用している前提)。

テスト対象

下記のようなThis is a sample.という文字列を返すだけのSampleモジュールをテストする。

sample/sample.module.tsimport { Module } from '@nestjs/common';
import { GetSampleUsecase } from './usecases/get-sample.usecase';
import { SampleController } from './sample.controller';

@Module({
  controllers: [SampleController],
  providers: [GetSampleUsecase],
})
export class SampleModule {}
sample/sample.controller.tsimport { Controller, Get } from '@nestjs/common';
import { GetSampleUsecase } from './usecases/get-sample.usecase';

@Controller('sample')
export class SampleController {
  constructor(private readonly getSampleUsecase: GetSampleUsecase) {}

  @Get()
  getSample(): string {
    return this.getSampleUsecase.handle();
  }
}
sample/usecases/get-sample.usecase.tsexport class GetSampleUsecase {
  handle(): string {
    return 'This is a sample.';
  }
}

Jestの設定

/
├── src
└── test
   ├── unit
   └── e2e
      └── sample

NestJSではデフォルトでユニットテストはsrc下に、E2Eテストはtest下に置くようになっているが、個人的な好みとしてどちらもtest下に配置する。

package.json"jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "test",
    "testEnvironment": "node",
    "testRegex": ".*\\..*spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
}

テストコード

1. 下準備

e2e/setup-e2e.tsimport { Test, type TestingModule } from '@nestjs/testing';
import {
  FastifyAdapter,
  type NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { mainConfig } from '@src/main.config';
import { EnvModule } from '@src/config/env/env.module';
import { DatabaseModule } from '@src/config/database/database.module';
import type { Type } from '@nestjs/common/interfaces/type.interface';
import type { DynamicModule } from '@nestjs/common/interfaces/modules/dynamic-module.interface';
import type { ForwardReference } from '@nestjs/common/interfaces/modules/forward-reference.interface';

type Output = {
  module: TestingModule;
  app: NestFastifyApplication;
};

export async function setupE2e(
  imports: (
    | Type<any>
    | DynamicModule
    | Promise<DynamicModule>
    | ForwardReference
  )[],
): Promise<Output> {
  const module = await Test.createTestingModule({
    imports,
  }).compile();

  const app = module.createNestApplication<NestFastifyApplication>(
    new FastifyAdapter(),
  );
  await app.init();
  await app.getHttpAdapter().getInstance().ready();

  return { module, app };
}

E2Eテストで共通となる下準備の処理を実装する。

ここで返されるmoduleappを各E2Eテストで使用し、そのモジュールのプロバイダーを取得したりリクエストを投げたりする。

const module = await Test.createTestingModule({
  imports,
  imports: [...imports, EnvModule, DatabaseModule],
}).compile();

もしAppModuleなどで環境変数やDB周りのモジュールをimportしている場合は、上記のようにimportsに追加する。

const app = module.createNestApplication<NestFastifyApplication>(
  new FastifyAdapter(),
);
app.useGlobalPipes(new ValidationPipe());
await app.init();
await app.getHttpAdapter().getInstance().ready();

もしグローバルなPipeやGuardをmain.ts内でバインドしている場合は、上記のようにここで設定しておく。

2. E2Eテストの実装

e2e/sample/sample.e2e-spec.tsimport { setupE2e } from '../setup-e2e';
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
import { HttpStatus } from '@nestjs/common';
import { SampleModule } from '@src/sample/sample.module';

describe('sample(E2E)', () => {
  let app: NestFastifyApplication;

  beforeAll(async () => {
    const { module, app: initializedApp } = await setupE2e([SampleModule]);
    app = initializedApp;
  });

  afterAll(async () => {
    await app.close();
  });

  describe('/sample(GET)', () => {
    it('GETリクエスト_サンプルのテキストが返る', async () => {
      // run
      const { statusCode, body } = await app.inject({
        method: 'GET',
        path: 'sample',
      });

      // assert
      expect(statusCode).toBe(HttpStatus.OK);
      expect(body).toContain('This is a sample.');
    });
  });
});

あとはそのappを使用しリクエストを投げて検証すれば実装完了。

npm run test e2e/sample/sample.e2e-spec.ts
 PASS  test/e2e/sample/sample.e2e-spec.ts (5.784 s)
  sample(E2E)
    /sample(GET)
      ✓ GETリクエスト_サンプルのテキストが返る (17 ms)

テストを実行し無事成功も確認できた。

let app: NestFastifyApplication;
let sampleRepository: SampleRepository;

beforeAll(async () => {
  const { module, app: initializedApp } = await setupE2e([SampleModule]);
  app = initializedApp;
  sampleRepository = module.get<SampleRepository>(SampleRepository);
});

もしそのモジュールのプロバイダーを使用したい場合はmoduleからget()すればよい。