1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Jest + Nest.jsで始める! E2Eテスト

Last updated at Posted at 2022-03-14

概要

NestJsJestによる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で実行できるようにしました

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?