【NestJS】結合テストを実装する(ConfigModule, cross-env)

#はじめに
単体テストに引き続き、結合テストを実装しました。

https://qiita.com/suzuki0430/items/d15c8642b46b57cdf91b

https://qiita.com/suzuki0430/items/697601370b078cc65fd6

https://qiita.com/suzuki0430/items/ac5ccc587e75a19bb567

実装したコードを確認したい方は以下よりご確認ください。

https://github.com/suzuki0430/car-api-nest

#実装
ユーザの動作の流れをテストするために、各モジュールを組み合わせた結合テストを作成します。

これまでCookieSessionやValidationPipeの設定は`main.ts`内で行っていましたが、テストを実行するときに`main.ts`は実行されないので、これらの設定を`AppModule`側に移してあげる必要があります。

まずValidationPipeについては、`@Module()`の`providers`で`AppService`の下に記述を加えます。

```ts:app.module.ts
  providers: [
    AppService,
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true,
      }),
    },
  ],
```

cookieSessionについては、`AppModule`クラス内に以下の記述を加えます。

```ts:app.module.ts
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(
      cookieSession({
        keys: ['key1'],
      }),
    );
  }
}
```

以下のテストケースで新規登録に関する結合テストを作成すると以下のようになります。

- `/auth/signup`にPOSTリクエストを投げて(新規登録を行って)、登録したユーザのidとemailがレスポンスとして返ってくるかどうか
- 新規登録後に`/auth/whoami`にGETリクエストを投げて、ログインユーザのemailがレスポンスとして返ってくるかどうか

```ts:auth.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('Authentication System', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('handles a signup request', () => {
    const email = 'asdlkjqdaa@akl.com';

    return request(app.getHttpServer())
      .post('/auth/signup')
      .send({ email, password: 'alsdk' })
      .expect(201)
      .then((res) => {
        const { id, email } = res.body;
        expect(id).toBeDefined();
        expect(email).toEqual(email);
      });
  });

  it('signup as a new user then get the currency logged in user', async () => {
    const email = 'asdf@asdf.com';

    const res = await request(app.getHttpServer())
      .post('/auth/signup')
      .send({ email, password: 'asdf' })
      .expect(201);

    const cookie = res.get('Set-Cookie');

    const { body } = await request(app.getHttpServer())
      .get('/auth/whoami')
      .set('Cookie', cookie)
      .expect(200);

    expect(body.email).toEqual(email);
  });
});
```
無事にテストが完成したようにみえますが、改善すべき点がいくつか存在します。

##テストデータ用のDBを用意する
現在の設定だと、テスト環境でも`db.sqlite`を使用しているため、テストデータと本番データが混ざってしまいます。
そのため、テスト時には使用するDBを切り替えるような設定が必要となります。

まずは`.env.development`と`.env.test`ファイルを作成し、`DB_NAME`をそれぞれ設定します。

```..env.development
DB_NAME=db.sqlite
```

```..env.test
DB_NAME=test.sqlite
```
これらの環境変数を取得する際に`ConfigModule`および`ConfigService`を使用するため、`@nest/config`をインストールします。

```
npm install @nestjs/config
```
`ConfigModule.forRoot()`でenvのファイル名を指定します。
環境によって参照するファイルを切り替えるために、`process.env.NODE_ENV`を使用しています。

```ts:app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: `.env.${process.env.NODE_ENV}`,
    }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => {
        return {
          type: 'sqlite',
          database: config.get<string>('DB_NAME'),
          entities: [User, Report],
          synchronize: true,
        };
      },
    }),
```
npmスクリプトを実行する際に任意の環境変数を設定するパッケージ`cross-env`をインストールします。

```
npm install cross-env
```
`cross-env NODE_ENV=test`などをスクリプトに加えてあげることで環境変数が設定されます。

```json:package.json
    "start": "cross-env NODE_ENV=development nest start",
    "start:dev": "cross-env NODE_ENV=development nest start --watch",
    "start:debug": "cross-env NODE_ENV=development nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "cross-env NODE_ENV=test jest --watch --maxWorkers=1",
    "test:cov": "cross-env NODE_ENV=test jest --coverage",
    "test:debug": "cross-env NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json"
```
試しに`npm run test:e2e`でテストを実行してみると、以下のエラーがでます。
![スクリーンショット 2021-10-13 22.14.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/91ad4623-35d7-0e70-c24d-d4c2aa5b5ebc.png)
このエラーは記述した2つのテストで各々が`test.sqlite`に接続しようとしているために起こります。
`test:e2e`のスクリプトに`--maxWorkers=1`を加えることで、接続がバッティングしないようになります。

```json
"test:e2e": "cross-env NODE_ENV=test jest --config ./test/jest-e2e.json --maxWorkers=1"
```

##テストごとにDBにデータがたまらないようにする
新規登録のテストを複数回行ったとき、1度目にユーザデータが作成されるため、2度目以降は失敗します。
テストの回数によって結果が変わってしまうのはよくないので、テストが終わったらDBを元の状態に戻すようにします。

`setup.ts`を作成し、テスト前`global.beforeEach()`でDBを削除する処理を記述します。
(node v14.14.0以降であれば`await rm(join(__dirname, '..', 'test.sqlite'));`)

そして、テスト後`global.afterEach()`にはDBへの接続を切断する処理を記述します。

```ts:setup.ts
import { join } from 'path';
import { promises } from 'fs';
import { getConnection } from 'typeorm';

global.beforeEach(async () => {
  try {
    await promises.unlink(join(__dirname, '..', 'test.sqlite'));
  } catch (err) {}
});

global.afterEach(async () => {
  const conn = getConnection();
  await conn.close();
});
```
`jest-e2e.json`に`"setupFilesAfterEnv": ["<rootDir>/setup.ts"]`を追記すれば設定完了です。

#参考資料
https://www.udemy.com/share/104SA43@KbgGpVb6kMoi3Lp819HRXD6a9QudBcjd1LUZmtWR6uofO1yLoLJrjqReEVevlYU0/

https://qiita.com/TakahiRoyte/items/c152ad8baa191ed1f8ae

https://zenn.dev/himorishige/articles/cfd7aedacc3083