NestJs + Jest で試験のプロセスを自動化した備忘録

# はじめに
<!-- 発端や概要を記載 -->
仕事で試験工数の削減とリファクタリングの一環でE2Eテストを導入してみました。

今回は実際に導入したフローをご紹介します！

# やり方

# 必要なパッケージをインストール
```bash
npm i --save-dev @nestjs/testing
```

# テストコードを作成

```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)
```