포시코딩

[Nest.js] Jest e2e test (app.e2e-spec.ts) 본문

Node.js

[Nest.js] Jest e2e test (app.e2e-spec.ts)

포시 2023. 3. 6. 15:52
728x90

개요

오늘도 헛소리 하는 다른 블로그들이 답답해서 내가 해결방법 찾아 정리해본다.

 

기본 세팅

app.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('AppController (e2e)', () => {
  let app: INestApplication;

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

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

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});

처음 Nest.js 프로젝트를 세팅하면 기본으로 이렇게 세팅되어 있을 것이다.

e2e 테스트를 무시하고 어느정도 개발했다는 상황으로 진행될 것이며

TypeOrmOptionFactory를 상속받는 TypeOrmConfigService를 만들어 사용하는 방식인 점 참고

사용한 파일들은 다음과 같다.

 

.env

DATABASE_HOST='localhost'
DATABASE_PORT=3306
DATABASE_USERNAME=''
DATABASE_PASSWORD=''
DATABASE_NAME=''
DATABASE_SYNC=true

 

TypeOrmConfigService

// ...import 생략
@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  constructor(private readonly configService: ConfigService) {}

  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: this.configService.get<string>('DATABASE_HOST'),
      port: this.configService.get<number>('DATABASE_PORT'),
      username: this.configService.get<string>('DATABASE_USERNAME'),
      password: this.configService.get<string>('DATABASE_PASSWORD'),
      database: this.configService.get<string>('DATABASE_NAME'),
      synchronize: Boolean(this.configService.get<string>('DATABASE_SYNC')),  // 배포 시 false
      entities: [// ...Entity들 생략],
    };
  }
}

 

app.module.ts

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, }),
    TypeOrmModule.forRootAsync({ useClass: TypeOrmConfigService }),
// ...생략

 

여기서 app.module.ts를 아래와 같이 수정하고

ConfigModule.forRoot({ 
  isGlobal: true,
  envFilePath: process.env.NODE_ENV === 'test' ? '.env.test' : '.env', 
}),

.env 파일을 복사해 .env.test 파일을 만들어

test 전용 DATABASE_NAME에 기존에 쓰는 DB 가 아닌 test용 DB를 바라보게 해준다. 

(test용 DB는 수동으로 만들어야함)

 

이렇게만 해주면 npm run test:e2e 를 실행할 때 알아서 NODE_ENV=test 인 상태로 실행하는데

만약 했는데 DB를 원래 DB로 바라본다면 

"test:e2e": "NODE_ENV=test jest --config ./test/jest-e2e.json"

이렇게 변경 후 테스트를 진행하자.

 

request 에러

시작하려고 딱 app.e2e-spec.ts 파일 열자마자 저렇게 빨간 밑줄 그어지며 에러가 나있는 상황이 있을 수 있다.

내가 그랬는데 아래와 같이 supertest를 import하는 부분을 * 대신 그냥 바로 request로 받게 변경하면 해결된다.

 

 

첫 e2e 테스트 - 테스트 후 앱 종료

이제 빨간줄이 없어졌으니

첫 e2e 테스트를 해보자

 

package.json

"test:e2e": "jest --config ./test/jest-e2e.json"

package.json에 나온대로 

npm run test:e2e

위 명령어를 실행하면 된다.

 

실행하면 이렇게 성공했다는걸 볼 수 있는데 밑에 노란글이 거슬린다.

읽어보면 테스트가 끝난 이후 제대로 종료되지 않고 있어서 그렇다는건데

 

아래와 같이 코드를 수정해주자.

 

app.e2e-spec.ts

// ...import 생략
describe('AppController (e2e)', () => {
  let app: INestApplication;

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

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

  afterAll(async () => {
    app.close();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World');
  });
});

기존에 beforeEach는 beforeAll로 바뀌었고, 

afterAll을 추가해서 app.close()를 시키게 해줬다.

 

다시 아까와 같이 테스트를 실행하면

노란글 없이 깔끔하게 테스트를 마치는 걸 볼 수 있다.

 

Entity 경로 문제

실행 시 위 에러처럼 Cannot find module 모듈을 찾을 수 없다면서

찾을 수 없는 entity를 가리킨다. 

검색해보면 대다수의 사람들이 상대경로로 바꾸면 해결된다고 하는데 

개소리니 다음과 같이 진행하면 된다.

 

app.e2e-spec.ts 파일 바로 옆에 붙어 있는

jest-e2e.json 파일을 다음과 같이 수정한다.

 

jest-e2e.json

"moduleNameMapper": {
  "^src/(.*)$": "<rootDir>/../src/$1"
}

세팅 후 다시 테스트 실행해보면 module 잘 찾으면서 성공하는 것을 볼 수 있다.

 

GET 테스트

이제 기존에 만들어놓은 get 요청에 대해 e2e 테스트를 진행해보자.

이미 만들어져 있는 기능 중 어떤 목록을 불러오는 get 요청을 할 것이다.

 

controller

대충 이렇게 생긴 controller 메서드인데 요약하자면 

/api/meetups 로 GET 요청을 보내면 array를 리턴하는 메서드이다.

p와 keyword를 인자로 받고 있지만 기본값을 세팅해놔서 따로 아무것도 추가해줄 필요 없다.

 

app.e2e-spec.ts

describe('/api/meetups', () => {
  it('GET 200', () => {
    return request(app.getHttpServer())
    .get('/api/meetups')
    .expect(200)
    .expect([]);
  });
});

app.getHttpServer()는 localhost:포트번호 를 안쓰기 위해 사용했다. 

보면 .get('/api/meeetups') 로 요청하고 있고

.expect(200) 을 통해 성공 시 200 status code를, 

.expect([]) 를 통해 성공 시 빈 array 값을 리턴받는걸 예상하게끔 세팅해줬다.

 

테스트 실행해서 성공하는거 확인

 

POST 테스트 

이제 POST 요청을 테스트해보자

describe('/api/auth', () => {
  const signupBodyDTO: SignUpBodyDTO = {
    email: 'test@gmail.com',
    password: 'qwer1234',
  }
  it('POST 201', () => {
    return request(app.getHttpServer())
    .post('/api/auth/signup')
    .send(signupBodyDTO)
    .expect(201)
    .expect({
      message: '회원가입 되었습니다.',
    });
  });
});

회원가입 하는 POST 요청이다.

기본적으로 GET 요청과 비슷한데 다른점은 .send()를 통해 파라미터를 세팅하고 있다는 점이다.

위에서 미리 signupBodyDTO를 만들어 넣은 파라미터 오브젝트를 만든 후 

.send()에 넣어주면서 POST 요청을 하고 있다.

 

또한 성공 시 201 status code와 

{ message: '회원가입 되었습니다.' }

오브젝트를 받기 때문에 위와 같이 세팅해줬다.

 

실행 시 아까의 GET 요청과 더불어 성공하는 것을 볼 수 있다.

 

id 5인건 내가 여러번 테스트하느라

근데 테스트 DB Table을 확인해보니 방금 POST 요청한 데이터가 저장된 채로 남아있다.

이럴경우 다시 e2e 테스트를 한다면

중요하지 않으니 작게

이렇게 에러가 날 것이다. 

테스트를 마칠 때 DB를 초기화 해주는 건 다음 항목에서 알아보자

 

Test DB Table 비우기

데이터가 그대로 남아있는건 테스트가 끝나면서 사리지게끔

따로 세팅을 안해줬기 때문인데 아래와 같이 수정하자.

 

app.e2e-spec.ts

import { Connection } from 'typeorm';

describe('AppController (e2e)', () => {
  let app: INestApplication;

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

    app = moduleFixture.createNestApplication();
    const connection = app.get(Connection);
    await connection.synchronize(true);
    await app.init();
  });

  afterAll(async () => {
    const connection = app.get(Connection);
    await connection.synchronize(true);
    app.close();
  });

beforeAll, afterAll 에서

const connection = app.get(Connection);
await connection.synchronize(true);

위 두 코드가 추가된 것을 볼 수 있다. 

Connection은 typeorm에서 가져온 애인데

 

빗금이 쳐져있고 살펴보면 더 이상 지원되지 않기에 is deprecated. 라고 되어있다.

좀 찾아본 결과 typeorm이 0.2x에서 0.3.x로 업데이트 되며 바뀐 부분이라 그렇다는데

다른 걸 쓰는 방법을 좀 찾아보았으나 실패하여 위 방법 그대로 진행. 

 

만약 다른 해결방법을 안다면 제발 알려주세요.

 

아무튼 이렇게 변경한 후 다시 테스트를 실행해보면

DB에 데이터 추가 없이 성공하는 것을 볼 수 있다.

 

정리

이제 나머지 API에 대해서는 위의 방법을 응용해 작성해보자.

사실 나도 위의 GET, POST 요청만 작성하고 바로 포스팅하는거라 이제 나머지 작성해야함.

 

위 글 이후 작성되는 코드에 대한건

https://github.com/chalkak2023/Chalkak-Backend

 

GitHub - chalkak2023/Chalkak-Backend

Contribute to chalkak2023/Chalkak-Backend development by creating an account on GitHub.

github.com

위 repo에서 확인이 가능할듯 싶다. 계속 업데이트중

 

도움받은 곳

수많은 Nest.js e2e 관련 글들 중에서 아래 두 분의 블로그 포스팅에서 많은 도움을 받아 작성됐다.

 

https://darrengwon.tistory.com/1049

 

Nest + Jest + supertest e2e test (1) : setting

* 테스트 코드를 작성할 공수가 제한적이라면 E2E만 하세요. 프론트엔드, 백엔드, E2E 모든 영역에서 테스트 코드를 작성 하면 좋겠지만 그 정도의 공수를 투입하는것은 현실적으로 힘든 일입니다

darrengwon.tistory.com

https://puleugo.tistory.com/114

 

[NestJS] e2e테스트 Jest 테스트 시 DB 초기화하는 법

e2e 테스트는 매번 데이터베이스를 초기화해줄 필요가 있습니다. 다만.. 저는 Jest를 쓰는데 따로 초기화 해주는 함수가 없었습니다. (있으면 알려주세요!) 그런 이유로 방법 계속 찾아봤는데 따로

puleugo.tistory.com

 

압도적 감사

728x90