App Configuration

테스트를 위한 환경변수와 DB 이원화

Testing

Dotenv

터미널에서 npm install @nestjs/config

dotenv 는 .env 파일과 일반적인 환경 변수파일을 참조하여 개체를 생성(겹칠시 환경변수 파일 우선)한다.

.env 파일은 배포에 포함되선 안된다. (git 에선 gitignore로 commit되지 않도록 해야)

우리는 개발용(배포용) .env 파일과 테스트용 .env 파일 두 개를 만들것이다.

root 디렉토리에 .env.tset 와 .env.development 파일 두개를 만들자.

1
2
// .env.development
DB_NAME=db.sqlite
1
2
// .env.test
DB_NAME=test.sqlite

AppModule에 아래와 같이 설정해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// app.module.ts
import { Module, ValidationPipe, MiddlewareConsumer } from '@nestjs/common';
import { APP_PIPE } from '@nestjs/core';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { UsersModule } from './users/users.module';
import { ReportsModule } from './reports/reports.module';
import { User } from './users/user.entity';
import { Report } from './reports/reports.entity';
const cookieSession = require('cookie-session');

@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'),
          synchronize: true,
          entities: [User, Report],
        }
      } 
    }),
    // TypeOrmModule.forRoot({
    //   type: 'sqlite',
    //   database: 'db.sqlite',
    //   entities: [User, Report],
    //   synchronize: true,
    // }),
    UsersModule,
    ReportsModule,
  ],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_PIPE,
      useValue: new ValidationPipe({
        whitelist: true,
      }),
    },
  ],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(
        cookieSession({
          keys: ['anything'],
        }),
      )
      .forRoutes('*');
  }
}

이제 서버 실행에 마다 알맞은 환경변수를 사용할 수 있도록 적용해주자

npm install cross-env 설치 후 package.json을 수정한다.

1
2
3
4
5
6
7
8
9
10
11
// package.json
"scripts": {

    "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",
    "test": "cross-env NODE_ENV=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"

위 코드들에만 앞에 cross-env NODE_ENV= 를 추가한다. 위는 개발, 배포 떄니까 development로 설정하고 아래는 테스트용이니 test로 설정한다.

이제 터미널 2개를 켜서 각각 npm run start:devnpm run test:e2e를 실행시켜보고, 중복된 아이디로 생성을 반복해서 생성이 거부되는지 확인해보자. 에러가 나야 제대로 작동한다는 뜻이다. 겨로가에 database is locked라고 뜰 텐데, 이제 이 부분을 해결할 것이다.

잠긴 데이터베이스 열기

우리 test 폴더내에는 jest가 app , auth 2가지 인스턴스가 생성되도록 구성되어있다. 각각의 인스턴스가 동일한 SQLite 데이터베이스에 동시에 연결을 시도할텐데, SQLite는 다수의 병렬 연결을 거부한다. 이를 수정하여 하나의 테스트만 실행하도록 하자.

package.json 파일에 가서

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

이 부분만 변경해주고 db.sqlite 파일을 삭제한 이후에 npm run test:e2e 를 실행해보면 더이상 dtabase가 잠겼다는 오류가 발생하지 않는다.

테스트 전 DB 정리하기

단일 테스트 전에 데이터를 삭제해야 제대로 테스트를 진행할 수 있다.

DB 내 모든 테이블의 행을 삭제하거나, 전체 DB를 삭제한 후 자동으로 DB를 다시 만들거나. 후자의 방법을 공부해보자.

쉽게 말해 test.sqlite를 삭제하도록 하면 되는것이다.

test 폴더의 jest 파일을 수정하자

1
2
3
4
5
6
7
8
9
10
{
  "moduleFileExtensions": ["js", "json", "ts"],
  "rootDir": ".",
  "testEnvironment": "node",
  "testRegex": ".e2e-spec.ts$",
  "transform": {
    "^.+\\.(t|j)s$": "ts-jest"
  },
  "setupFilesAfterEnv": ["<rootDir>/setup.ts"] // 추가한 코드
}

테스트 할 때마다 setup.ts를 실행하게 된다. test디렉토리에 저 파일을 만들자

1
2
3
4
5
6
7
8
9
// setup.ts
import { rm } from 'fs/promises';
import { join } from 'path';

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

이제 npm run test:e2e2번 실행해보자. 매번 DB가 삭제되므로 두번 해도 통과해야한다.

테스트 추가

다시 돌아와서, 인증(authentication) 관련 테스트 코드를 추가하자. 테스트 폴더로 간다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// auth.e2e-spec.ts
it('signup as a new user then get the currently logged in user', async () => {
    const email = 'asdf@asdf.com';

    const res = await request(app.getHttpServer())
      .post('/auth/signup')
      .send({ email, apssword: '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);
  });
});

테스트를 통과하는지 확인해보자.