지난 블로그에서 NestJS가 지원하는 Test Framework인 Jest를 활용하여 Mocking 기반의 Unit Test를 작성했습니다. 이번엔 QueryBuilder와 Transaction을 활용하는 경우 Unit Test를 어떻게 작성하는지 살펴보도록 하겠습니다.
Repository의 의존성을 Mocking하여 외부환경에 영향을 받지 않는, 독립된 Test 환경을 조성했습니다. 그러나 다양한 기능과 복잡한 로직을 구현하다 보면 Repository외에 QueryBuilder, Connection 등 의존성을 주입받은 다양한 모듈과 라이브러리를 사용하게 됩니다. 이런 객체들의 Mocking을 어떻게 구현하는지 알아보겠습니다.
지난번
getMfr
Service Logic은 단순히User
Repository의find
method만 사용했습니다. 이번엔getOrderQtyDetail
Service를 작성하며innerJoin
,leftJoin
이 여러번 사용되어 QueryBuilder를 활용했습니다.
getOrderQtyDetail
API가 사용되는 Layout은 다음과 같습니다.
Path Parameter로 OrderId를 받고, 그 요청에 대해 특정 Order의 Product에 대해서 Size 별 SKU, Barcode, 주문량과 선적수량, 도착수량과 그 합계들을 함께 반환해야 합니다. 그에 맞게 작성한 getOrderQtyDetail
Service는 아래와 같습니다.
orderId
에 대한 Order의 존재 유무 예외처리를 시작으로 QueryBuilder를 활용해 Order
, Product
, Barcde
등 Table을 모두 조인하여 필요한 결과만 뽑아냅니다. 그 후 뽑아낸 결과를 활용해 주문량, 선적수량, 도착수량 별 합계를 계산한 후 결과에 함께 반환합니다.
이제 코드가 실행되는 Service 환경과 같은 Test 환경을 만들어봅시다. 먼저 getOrerQtyDetail
은 Order Service에 속하므로 Order Module과 같은 환경을 만들기 위해 Provider들을 Mocking해줘야 합니다.
Order Module에서 Import된 Repository들을 사용하기 위해 Testing Module에서도 Mocking된 Value를 사용하는 Repository를 공급합니다.
이제 QueryBuilder를 Mocking해주어야 합니다. QueryBuilder의 Method인 innerJoin, leftJoin, select 등의 특징은 바로 Chaining Function이라는 점입니다. 이런 Chain 특징을 Mocking하기 위해 mockReturnValue()
와 mockReturnThis()
를 활용합니다.
mockReturnThis()
는 jest.fn(function () {return this;})
의 Sugar Function으로, this
를 반환하여 chained method를 mocking하는 것이 가능하도록 해줍니다.
이제 getOrderQtyDetail
의 Test Code를 살펴보겠습니다.
Order
의 예외처리에 대한 try, catch 사용은 지난번 Test와 동일합니다. 그 후 createQueryBuilder
의 호출 여부를 확인하는 Test Case를 작성합니다. innerJoin
은 3번, leftJoin
은 2번 호출되었는지 toHaveBeenCalledTimes
method로 확인합니다.
QueryBuilder의 method들이 호출되었음을 확인한 후, getRawMany
의 반환값을 mocking한 후 getOrderQtyDetail
의 반환값과 비교합니다. 최종 테스트 결과는 아래와 같습니다.
NestJS에서 Transaction은 QueryRunner와 Connection을 활용합니다. Transaction을 Test하기 위해 Connection 객체와 QueryRunner를 어떻게 mocking하는지 알아봅시다.
modOrderStatus
modOrderStatus
는 Request로 받은 order들의 orderStatus를 변경해주는 기능입니다. 모든 update가 성공적으로 완료되어야 commit 하고, 도중에 실패한다면 모든 update를 rollback 하는 transaction을 적용했습니다. modOrderStatus
함수는 다음과 같습니다.
마찬가지로 Test 환경을 modOrderStatus
가 작성된 환경과 동일하게 만들어줍시다. orderRepository 이외에 추가적으로 Connection
을 주입하여 사용하고 있기때문에 Connection
객체와 QueryRunner
를 Mocking 해줍니다.
Repository, QueryBuilder를 Mocking했던 것과 마찬가지로 Jest Mocking Function을 활용해 QueryRunner의 각 method를 Test 환경에서 독립적으로 사용할 수 있도록 정의해줍니다. 이제 Test Case를 크게 3개로 나누어 작성해봅시다.
먼저 OrderStatus를 수정하기 위해 요청받은 OrderId 리스트의 존재여부를 확인하고 예외처리 하는 과정에 대해 Test 합니다. 그 후 예외처리가 끝나고 QueryRunner가 호출되는 것을 확인합니다.
QueryRunner와 startTransaction이 실행되어 Transaction이 적용되었습니다. Transaction Test에서 중요하다고 생각하는 것은 Transaction이 실패했을 경우 rollback이 적용되는 것을 확인하고, 성공했을 경우 commit이 실행되는 것을 확인하는 것입니다.
Transaction이 실패했다고 가정하기 위해, spyOn
함수를 활용해 findOne
method로 반환된 값이 undefined
가 되도록 mocking합니다. 이제 수정하고자 하는 Order를 찾지 못하고 Error를 반환하여 Transaction이 실패할 것입니다.
Transaction이 성공하면 update
method와 commitTransaction
을 호출해야 합니다. 마찬가지로 Transaction이 성공했다고 가정하기 위해 findOne
method의 반환값이 존재하도록 mocking합니다.
Transaction이 성공하여 순차적으로 update
, commitTransaction
을 호출하고 isSuccess
가 true
를 반환하는 것을 확인합니다. 전체 TestCode와 결과는 다음과 같습니다.
이렇게 Test의 독립된 환경을 만들기 위해 QueryBuilder, QueryRunner, Connection을 Mocking하고, spyOne
과 mockResolvedValue
를 적절하게 활용하여 테스트하고자 하는 함수의 Logic의 분기점을 설정하여 TestCase를 작성할 수 있었습니다. 적절한 Parameter와 Return Value를 설정해준다면 Input과 Output에 대한 Test도 가능하지만, Logic에 중요하게 작용하는 함수나 Method의 호출 여부만을 Test했습니다.