たかぎとねこの忘備録

プログラミングに関する忘備録を自分用に残しときます。マサカリ怖い。

NestJSでe2eテストをやってみる

ルートディレクトリに、testディレクトリを作成する。 e2eテストのファイル名はxxx.e2e-spec.tsの形式にする。

Jestの設定

package.jsonjestの設定を修正する。

testRegexフィールドを文字列のみから配列に修正して、e2e-specファイルもテストできるようにする。

// package.json

...

"jest": {
  ...
  "testRegex": [
      ".*\\.spec\\.ts$",
      ".*\\-spec\\.ts$"
    ],
}
...

moduleNameMapperの設定をtsconfig.jsonpathsに設定した内容と同じにする。

例えば、tscofnig.jsonpaths"~/*": ["./src/*"]を追加する。

// tsconfig.json
...
"compilerOptions": {
  ...
  "paths": {
    "~/*": ["./src/*"]
  }
}
...

書き方は少し異なるが、内容としては同じように~/auth/...などでモジュールをインポートしてもエラーにならないように設定する。

// package.json

...
"jest": {
  ...
  "moduleNameMapper": {
    "^~/(.+)$": "<rootDir>/src/$1"
  },
  ...
}
...

コントローラーを準備する

先にUsersModuleを作成済みで、nest g controller usersでコントローラーも作成済みであることを前提にする。

JwtAuthGuardはBearerトークンの検証を行なってくれるJwtStrategyクラスのvalidateを呼び出すために必要なガード。

// src/users/users.controller.ts

import {
  Controller,
  Get,
  Param,
  UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { JwtAuthGuard } from '~/auth/guards/jwt-auth.guard';

@Controller('users')
export class UsersController {
  constructor(
    private readonly usersService: UsersService,
  ) {}

  @UseGuards(JwtAuthGuard)
  @Get(':id')
  async findOne(@Param('id') id: string) {
    const user = await this.usersService.findOne({
      id,
    });
    return user;
  }
}

テストを作成する

createTestingModule()メソッドを使用してTestingModuleインスタンスを取得する。

createTestingModule()に渡す引数は@Module()デコレーターに渡すオブジェクトと同じ内容。

最後に必ずcompile()メソッドを呼び出すのを忘れない。このメソッドを呼び出すことでNestFactory.create(AppModule)メソッドを呼び出したときと同じように依存関係を持つモジュールを解決してテスト用の準備が整ったモジュールを返す。

// test/users.e2e-spec.ts
...
beforeEach(async () => {
  const module = await Test.createTestingModule({
    imports: [UsersModule, AuthModule],
    providers: [UsersService, PrismaService],
}).compile();
...

取得したモジュールのcreateNestApplication()メソッドを呼び出して、Nestの実行環境をインスタンス化させる。これで、HTTPリクエストのシミュレーションが可能となる。

// test/users.e2e-spec.ts

...
app = module.createNestApplication();
await app.init();
...

実際のテストはSupertestのrequest()関数を使用してHTTPのテストを行なっていく。

request()にNestJSのコアであるHTTPリスナーへの参照を渡す。これは実行中のNestJSアプリケーションにルーティングさせるためである。 なのでrequest(app.getHttpServer())という呼び出し方になっている。

request()が呼び出されると、ラップされたHTTPサーバーがNestJSアプリに接続される。 これにより、実際のHTTPリクエストをシミュレートするためのメソッド(getpost)などが公開される。

// test/users.e2e-spec.ts

it(`/GET users`, async () => {
  const id = faker.datatype.uuid();
  const email = faker.internet.email();
  // ユーザーを作成する
  const user = await usersService.create({
    id,
    email,
    name: faker.name.firstName(),
  });
  // アクセストークンを作成する
  const payload = {
    sub: user.id,
  };
  const { access_token } = await authService.sign(payload);

  await request(app.getHttpServer())
    .get(`/users/${user.id}`)
    .set('Authorization', `Bearer ${access_token}`)
    .expect(200)
    .expect((res) => {
      expect(res.body).toBeDefined();
    })
    .timeout(60000);

  // ユーザーを削除する
  await usersService.remove({
    id: user.id,
  });
});

UsersControllerでヘッダーにAuthorizationヘッダーが設定されていて、取得できたペイロードに設定されているsubに既存のユーザーのIDがセットされているかを確認しているため、アクセストークンを作成している。

そのため、AuthServiceでは署名用のメソッドを実装している。

// src/auth/auth.service.ts

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  constructor(private jwtService: JwtService) {}

  async sign(payload: { sub: string }) {
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}

もし、テスト内でimports配列に指定したモジュールが公開しているサービスを利用したい場合はmodule.get()を利用する。

// test/users.e2e-spec.ts

...
describe('Users', () => {
  let app: INestApplication;
  let usersService: UsersService;
  let authService: AuthService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      imports: [UsersModule, AuthModule],
      providers: [UsersService, PrismaService],
    }).compile();

    app = module.createNestApplication();
    await app.init();

    usersService = module.get<UsersService>(UsersService);
    authService = module.get<AuthService>(AuthService);
  }, 60000);
...