NestJSでe2eテストをやってみる
ルートディレクトリに、test
ディレクトリを作成する。
e2eテストのファイル名はxxx.e2e-spec.ts
の形式にする。
Jestの設定
package.json
のjest
の設定を修正する。
testRegex
フィールドを文字列のみから配列に修正して、e2e-spec
ファイルもテストできるようにする。
// package.json ... "jest": { ... "testRegex": [ ".*\\.spec\\.ts$", ".*\\-spec\\.ts$" ], } ...
moduleNameMapper
の設定をtsconfig.jsonのpaths
に設定した内容と同じにする。
例えば、tscofnig.jsonでpaths
に"~/*": ["./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リクエストをシミュレートするためのメソッド(get
やpost
)などが公開される。
// 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); ...