NestJS 10 | Unit Test - JEST

임종성·2021년 9월 15일
4

NestJS

목록 보기
10/13
post-thumbnail

지금까지는 NestJS Framework가 제공하는 다양한 기능을 적용하여 API를 구현했습니다. 기능 구현도 중요하지만, Unit Test는 기능을 갖춘 Software의 필수 부분입니다. Unit Test의 중요성을 다시 곱씹어보며 NestJS가 제공하는 Test Utility와 함께 Test Case를 작성해보겠습니다.

JEST

Nestjs에서는 Javascript 테스트 프레임워크인 jest를 기본 테스트 프레임워크로 지원하고 있습니다. 테스트 코드의 모양이 직관적이고 문서화가 잘되어 있어 요즘 많이 활용되고 있는 Framework로서, 여러가지 상황을 설정하고 그 상황에 맞는 로직과 결과가 나오는지 자동으로 테스트해줍니다.

Jest 이전에는 여러가지 테스트 라이브러리를 섞어 사용했습니다. Mock 함수를 만들기 위해 Sinon과 TestDouble같은 Test Mock 라이브러리를 추가로 설치하여 사용하는 것이 그 예입니다. 그러나 Jest를 사용하면 거의 모든 기능을 한 번에 지원하기 때문에 아주 효과적인 Test Framework라고 생각합니다.

Mocking

Jest를 사용할 때 장점 중 하나는 다른 라이브러리 설치 없이 바로 mock 기능을 지원한다는 점입니다!

What is Mocking?

Mocking은 단위 테스트를 작성할 때 해당 코드가 의존하는 부분을 가짜(mock)으로 대체하는 기법을 말합니다. 일반적으로 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 너무 부담스러운 경우 mocking이 많이 사용됩니다.

간단한 예로 들면 Database에서 데이터를 삭제하는 코드에 대한 단위 테스트를 작성할 때, 실제 데이터베이스를 사용한다면 여러가지 문제점이 발생할 수 있습니다. 데이터베이스와의 연동, 트랜잭션, 쿼리 전송 등 테스트를 위한 코드보다 테스트 환경을 조성하는 데 더 많은 노력이 필요하게 됩니다. 또한 테스트가 데이터베이스의 연결 상태, 즉 외부 환경에 영향을 받게 됩니다.

이런 방식으로 테스트를 작성하게 되면 특정 기능만 분리해서 테스트하겠다는 단위 테스트(Unit Test)의 근본적인 사상에 부합하지 않게 됩니다.

Mocking은 이러한 상황에서 실제 객체인 척하는 가짜 객체를 생성하는 매커니즘을 제공합니다. 또한 테스트가 실행되는 동안 Mocking 객체가 호출되거나, 어떤 아웃풋을 반환하는지 등을 기억하기에 어떻게 사용되는 지 검증이 가능합니다. 따라서 Mocking을 이용하면 구체적으로 구현해야 하는 실제 객체 사용보다 훨씬 빠르고, 동일한 결과를 내는 테스트를 작성 가능합니다.

NestJS Unit Test

Unit Test의 핵심은 내가 만든 가장 작은 Unit을 테스트 하는 것입니다.

NestJS는 Dependency Injection을 통해 각 Module을 캡슐화하여 서로의 의존성을 최대한 배제하고 주입하여 사용하는 특징이 있습니다. 따라서 NestJS의 Test 환경을 조성할 경우 의존성 주입을 하지 않고 의존성 자체를 Mocking 해야 합니다.

예를 들어 UserController를 Test한다고 가정하면 실제로 UserController에 영향을 주는 UserService와 같은 Provider들의 의존성을 신경쓰지 않고, 그저 Test에 사용될 Mocking UserService를 사용하여 독립된 환경의 Controller를 테스트해야 합니다.

Test Environment

Unit Test를 하기 위해 실제 코드가 실행되는 환경과 같은 환경을 조성해줘야 합니다. 예시로 UserService를 테스트 하기 위해 User Module과 동일한 환경을 만들어보겠습니다. 실제 UserService의 Module은 아래와 같은 Provider를 가지며, 이 Provider를 Service에서 사용하게 됩니다.

또한 UserService는 아래와 같이 UserReopsitory를 Injection받아 사용합니다. 이때 만약 실제 Repository를 사용하며 동일한 환경을 만들어준다면 실제 DB에 데이터가 들어가게 되어 심각한 오류를 초래할 수도 있습니다.

따라서 UserRepository를 Mocking하여 독립된 환경에서 Service를 테스트할 수 있도록 합니다!

예시에서 UserRepository가 사용할 Method는 find method 하나이기에 위와 같이 함수를 mocking해줍니다. MockRepository는 다른 TypeOrm의 Entity들이 공유해서는 안되기 때문에 함수 형태로 작성합니다.

getMfr Unit Test

이제 본격적으로 UserService의 함수에 대한 Unit Test를 작성해봅시다. Test의 예시로 만든 getMfr() 함수는 User Type이 Manufacturer인 모든 User의 이름을 불러와 배열 형태로 반환하는 함수입니다. 코드는 다음과 같습니다.

Unit Test를 통해 확인하고 싶은 것은 Output이 제대로 나오냐보다는 Logic이 잘 구현되었냐는 것입니다. 즉find method가 정상적으로 호출되고, manufacturer가 존재하지 않을 경우 정상적으로 예외 처리를 해주는지에 대한 Test를 할 것입니다. 이에 대한 테스트 코드는 다음과 같습니다.

  • describe()는 여러개의 it()을 하나의 Test 작업단위로 묶어주는 API라고 볼 수 있습니다. 하나의 작은 TestCase를 it()라고 한다면 describe()는 여러개의 TestCase를 하나의 그룹으로 묶어주는 역할을 합니다. 여러개의 describe()을 묶어줄 수도 있습니다.

  • beforeEach()는 TestCase의 각 코드가 실행되기 전에 수행되어야 하는 로직을 넣는 API 입니다. 반복되는 Logic을 넣을 때 사용됩니다.

  • mockResolvedValue([{ userName: 'LMF' }]);는 Jest Function의 Resolved Value를 정해줍니다. 이를 통해 Mocking한find Method의 결과값이 { userName: 'LMF' }로 나오도록 조정할 수 있습니다.

  • expect()를 활용해 테스트 기대값과 실제 결과를 비교하여 TestCase의 성공여부를 결정합니다.

  • toHaveBeenCalledWithexpect에 넣은 함수의 호출 여부와 어떤 Paramter가 사용되었는지 확인합니다.

  • Exception Handling이 잘 되었는지 확인하기 위해 mockResolvedValue(undefined)를 사용하여 일부러 예외를 발생시킵니다. try, catch를 사용하여 실제로 Error Message가 발생하지 않도록 해주고 Error의 종류와 메시지가 정상적으로 반환되는지 확인합니다.

  • jest.spyOn() Mocking 함수를 이용해 scmUserRepositoryfind method를 Mocking합니다. jest.spyOn()은 함수의 구현을 가짜로 대체하지 않고, 해당 함수의 호출 여부와 어떻게 호출되었는지만을 알기 위해 사용합니다. 보통 테스팅해야 하는 함수가 테스트할 다른 함수 내에서도 사용되어, mocking을 할 경우 사용되는 함수를 테스트할 수 없는 경우에 jest.spyOn() 함수를 사용합니다. 이 경우에는 mockResolvedValue를 사용하여 결과값을 Mocking하여 getMfr()의 기대값과 실제 결과가 일치하도록 했습니다.

이렇게 Unit Test를 작성해서 find Method의 호출여부와 예외처리, 그리고 Output을 확인하는 Logic을 테스트해보았습니다. npm run test:watch 명령어로 Test를 실행한 결과는 아래와 같습니다! describe()it()의 적절한 구문 작성으로 직관적인 Unit Test를 작성해봤습니다.


TIL

지금 보면 Jest가 익숙하지만, 처음 Test를 작성할 때는 정말정말 이해가 안가고 어려웠습니다. Mocking에 대한 개념도 모호하고, Javascript가 익숙치 않다 보니 jest.fn()을 파헤쳐 이해하는 것이 힘들었습니다. 이번 예시는 아주 간단한 테스트지만, QueryBuilder와 Transaction 관련 Unit Test를 작성할때는 정말 보지 않은 글이 없을 정도로 열심히 구글링을 했습니다. 실제로 기능을 구현하는 시간보다 테스트 작성에 걸린 시간이 훨씬훨씬 긴 것 같습니다.

Unit Test를 작성하면서 계속해서 고민하고 생각해본 것은, 어디까지 구체적으로 구현해야 되는가였습니다. Django Unit Test를 작성했을 때는 실제로 Test용 DB가 존재했기에 Mocking은 소셜로그인과 같이 대체할 수 없는 경우에만 사용하고 나머지 API에 대해서는 가짜 데이터를 만들어 테스트했습니다. 하지만 Jest를 사용하여 모든 객체와 함수, Provider에 대해 Mocking을 해서 고립된 환경을 조성해야 하니 참 생각이 많아졌습니다.

의존성 주입과 관련된 에러와, 함수가 정의되지 않았다는 에러를 수십 수백번 겪으며 Test Module의 DI 관련 문제는 익숙하게 해결할 수 있게 되었습니다. 다만 Input, Output Test를 할 것인가 아니면 함수의 호출정도만 테스트할 것인가에 대한 고민은 우선 호출여부 테스트를 하는 것으로 결정지었습니다. 개발자들 사이에서도 이에 대한 의견이 분분하다기에.. 우선 호출여부 확인하는 것으로 만족해야 할 것 같습니다.

하나 아쉬운 점은 TDD 방법론을 사용하며 프로젝트를 진행하고 싶었는데, 현실적으로 그럴만한 능력이 되지 않아 어쩔 수 없이 기능을 먼저 구현하고 테스트를 작성했다는 것입니다. 좀 더 Javascript의 핵심을 공부하고 익숙해지면 NestJS로 제대로 된 TDD가 가능하지 않을까.. 생각해봅니다.


참고자료
Mocking이란
Nest+Jest
gitBook
Nest 단위테스트 작성하기

profile
어디를 가든 마음을 다해 가자

0개의 댓글