概要
NestJs
でJest
によるE2E
テストを導入したので知見をまとめる
ディレクトリ構造
e2e
│ ├── users.e2e-spec.ts
│ ├── jest-e2e.json
│ ├── mocks
│ │ └── contractorReps
│ │ └── mock.ts
│ └── utilities
│ ├── common.utility.ts
│ └── master.utility.ts
設定用のjsonファイルを作成
-
moduleFileExtensions
:モジュールが使用するファイル拡張子の配列 -
rootDir
:Jestの設定ファイルが置かれているディレクトリ -
testEnvironment
:テスト環境 -
testRegex
:Jestがテストファイルを検出する際に使用するパターン -
transform
:パス、トランスフォーマーへのマップ -
moduleNameMapper
:試験対象のファイルパス
/e2e/jest-e2e.json
{
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../../$1"
}
}
TypeORM用のテスト設定ファイルを作成
TypeORMにはmigrationを自動的に行う機能が存在しないので、synchronize: true
とし強制的にmigrationを実行させています
本番環境のデータが失われる可能性があるので必ず本番用の設定ファイルではsynchronize: false
とすること
typeormが持つsynchronizeの機能を使えば、ヒトがテーブルを作成せずともスキーマさえあればテーブル定義を自動生成してくれる。
同期-アプリケーションの起動ごとにデータベーススキーマを自動作成する必要があるかどうかを示します。このオプションには注意してください。本番環境では使用しないでください。本番環境のデータが失われる可能性があります。このオプションは、デバッグおよび開発中に役立ちます。
ormconfig.test.ts
module.exports = {
type: 'mysql',
host: process.env.DB_HOST || 'xxxx',
port: process.env.DB_PORT || 'xxxx',
username: process.env.DB_USERNAME || 'xxxx',
password: process.env.DB_PASSWORD || 'xxxx',
database: process.env.DB_NAME || 'xxxx',
// アプリケーション実行時にEntityをデータベースに同期する
synchronize: true,
// 実行されるSQLをログとして吐く
logging: true,
entities: ['src/domain/entities/*.ts'],
migrations: ['src/databases/migrations/*.ts'],
seeds: ['src/test/databases/seeders/*.seed.{js,ts}'],
subscribers: ['src/subscribers/**/*.ts'],
cli: {
migrationsDir: 'src/databases/migrations',
entitiesDir: 'src/domain/entities',
seedersDir: 'src/databases/seeders',
subscribersDir: 'src/subscribers',
},
}
テスト用モジュールのインストール
bash
npm i --save-dev @nestjs/testing
テストするAPIについて
以下の形式でリクエストを投げるとユーザー情報を返すAPIをテストしてみます
リクエストデータ
http://localhost:xxxx/users?name=テスト
レスポンスデータ
{
"statusCode": 200,
"message": "SUCCESS",
"data": [
{
"id": 1,
"name": "テスト",
"ins_ts": "2021/11/29 13:47"
}
}
テストコード
バリデーションテストは割愛しています
users.e2e-spec.ts
import { INestApplication, ValidationPipe } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { APP_GUARD } from '@nestjs/core'
import { Test, TestingModule } from '@nestjs/testing'
import { TypeOrmModule } from '@nestjs/typeorm'
import * as request from 'supertest'
describe('サンプルテスト(E2E)', () => {
// モジュール設定開始
let app: INestApplication
// テスト実行時に毎回実行します
beforeEach(async () => {
// DBに接続&内部のデータをリフレッシュ
await useRefreshDatabase()
// ダミーデータをシードする
await runTestDataSeeder()
})
// テスト前に1回だけ実行します
beforeAll(async () => {
// モジュールでDIしている対象を定義します
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forFeature([User),
// テスト用の設定ファイル読み込み
ConfigModule.forRoot({
envFilePath: ENV_FILE_PATH,
}),
// メインモジュールを読み込む
AppModule,
],
controllers: [UserController],
// アクセス権限Guardをセットします
providers: [
ContractorService,
{
provide: APP_GUARD,
useExisting: RoleGuard,
},
RoleGuard,
],
}).compile()
// モジュールからインスタンスを作成します
app = moduleFixture.createNestApplication()
// Auto-validation#
app.useGlobalPipes(new ValidationPipe())
// モジュールの初期化
await app.init()
})
// テスト後に1回だけ実行します
afterAll(async () => {
// テスト終了
await app.close()
// DBとの接続を終了する
await tearDownDatabase()
})
/**
* @summary ユーザー一覧を取得します
* @param account
* @returns request.Response
*/
const index = async (account?: E2eLoginData): Promise<request.Response> => {
const res = account
? await request(app.getHttpServer())
.get(API_END_POINTS.USER)
.set(
'Authorization',
`Bearer ${await getJwtToken(request, app, account)}`
)
: await request(app.getHttpServer()).get(API_END_POINTS.USER)
return res
}
/**
* @summary ユーザーを取得します
* @param account
* @param id
* @returns request.Response
*/
const show = async (
id: number,
account?: E2eLoginData
): Promise<request.Response> => {
const res = account
? await request(app.getHttpServer())
.get(`${API_END_POINTS.USER}/${id}`)
.set(
'Authorization',
`Bearer ${await getJwtToken(request, app, account)}`
)
: await request(app.getHttpServer()).get(
`${API_END_POINTS.USER}/${id }`
)
return res
}
/**
* @summary ユーザーを作成します
* @param account
* @param body
* @returns request.Response
*/
const create = async (
dto: CreateUserDto,
account?: E2eLoginData
): Promise<request.Response> => {
const res = account
? await request(app.getHttpServer())
.post(API_END_POINTS.USER)
.set(
'Authorization',
`Bearer ${await getJwtToken(request, app, account)}`
)
.set('Accept', 'application/json') // responseデータをjsonとして扱ってください
.send(dto)
: await request(app.getHttpServer())
.post(API_END_POINTS.USER)
.set('Accept', 'application/json')
.send(dto)
return res
}
/**
* @summary ユーザーを更新します
* @param account
* @param id: number,
* @param body
* @returns request.Response
*/
const update = async (
dto: UpdateUserDto,
id: number,
account?: E2eLoginData
): Promise<request.Response> => {
const res = account
? await request(app.getHttpServer())
.patch(`${API_END_POINTS.USER}/${id}`)
.set(
'Authorization',
`Bearer ${await getJwtToken(request, app, account)}`
)
.set('Accept', 'application/json') // responseデータをjsonとして扱ってください
.send(dto)
: await request(app.getHttpServer())
.patch(`${API_END_POINTS.USER}/${id}`)
.set('Accept', 'application/json')
.send(dto)
return res
}
/**
* @summary ユーザーを論理削除します
* @param account
* @param id: number,
* @param body
* @returns request.Response
*/
const softDelete = async (
id: number,
account?: E2eLoginData
): Promise<request.Response> => {
const res = account
? await request(app.getHttpServer())
.delete(`${API_END_POINTS.USER}/${id}`)
.set(
'Authorization',
`Bearer ${await getJwtToken(request, app, account)}`
)
: await request(app.getHttpServer()).delete(
`${API_END_POINTS.USER}/${id}`
)
return res
}
describe('ユーザー一覧テスト', () => {
it('OK /users(GET)', async () => {
const res = await index(LOGIN_DATA.SERVICE_ADMIN)
expect(res.status).toEqual(HTTP_STATUS_CODES.OK)
expect(res.body).toEqual(INDEX_USERS)
})
})
describe('ユーザー編集テスト', () => {
it('OK /users/:id (GET)', async () => {
const id = (await index(LOGIN_DATA.SERVICE_ADMIN)).body[0]
.id
const res = await show(id, LOGIN_DATA.SERVICE_ADMIN)
expect(res.status).toEqual(HTTP_STATUS_CODES.OK)
expect(res.body).toEqual(SHOW_USER_DATA)
})
})
describe('ユーザー登録テスト', () => {
it('OK /users (POST)', async () => {
const body: CreateUserDto = {
name: 'hoge',
password: 'password',
password_confirm: 'password',
}
const res = await create(body, LOGIN_DATA.SERVICE_ADMIN)
expect(res.status).toEqual(HTTP_STATUS_CODES.CREATED)
expect(res.body.message).toEqual(RESPONSE_MESSAGES.USER)
})
})
describe('ユーザー更新テスト', () => {
it('OK users/:id (PATCH)', async () => {
const id = (await index(LOGIN_DATA.SERVICE_ADMIN)).body[0]
.id
const body: UpdateUserDto = {
name: 'fuga',
password: 'password',
password_confirm: 'password',
}
const res = await update(
body,
id,
LOGIN_DATA.SERVICE_ADMIN
)
expect(res.status).toEqual(HTTP_STATUS_CODES.OK)
expect(res.body.message).toEqual(
`ユーザーID「${id}」の更新に成功しました。`
)
})
})
describe('ユーザー論理削除テスト', () => {
it('OK /users/:id (DELETE)', async () => {
const id = (await index(LOGIN_DATA.SERVICE_ADMIN)).body[0]
.id
const res = await softDelete(contractor_rep_id, LOGIN_DATA.SERVICE_ADMIN)
expect(res.status).toEqual(HTTP_STATUS_CODES.OK)
expect(res.body.message).toEqual(
`ユーザーID「${contractor_rep_id}」の論理削除に成功しました。`
)
})
})
テストを実行
bash
# 2. E2E テストを実行する
npm run test:e2e
# 成功
PASS src/test/e2e/user.e2e-spec.ts (33.623 s)
サンプルテスト(E2E)
ユーザー一覧テスト
✓ OK /users(GET) (1748 ms)
ユーザー編集テスト
✓ OK /users/:id (GET) (1735 ms)
ユーザー登録テスト
✓ OK /users (POST) (1493 ms)
ユーザー更新テスト
✓ OK users/:id (PATCH) (1327 ms)
ユーザー論理削除テスト
✓ OK /users/:id (DELETE) (1557 ms)
E2EテストをGitHub Actionsで実行できるようにしました