많은 분들이 프로젝트를 진행하면서 때때로 API 개발에만 집중하는 경우가 있습니다. 저또한 개발을 할 때마다 항상 Test Code는 기본 POC가 구현되면 나중에 작성해야지 하는 마음이 저도 모르게 앞섭니다.
그러다보니 개발을 하던 중 예상치 못한 예외들을 만나는 경우가 상당히 많았습니다. 또한 “이러다 영영 Test Code를 못 써보는거 아닐까?” 라는 무심코 생각이 들어 이번 개발은 TDD로 진행해보고자 큰 결심을 하게 되었습니다.
앞의 내용들을 각설하고, 위와 같은 이유로 CI를 세팅할 때 Test Coverage가 70%미만이면 PR, Merge가 되지 않도록 파이프라인을 구축하였습니다.
단위테스트는 간단하게 말하자면 즉 개별 함수나 메서드가 예상대로 동작하는지 검증하는 테스트 방법입니다.
특히 다양한 입력값에 대한 응답이 예상과 일치하는지, 예외 상황에서 적절한 처리를 하는지를 주로 검증합니다.
간단한 예시를 통해 좀 더 자세히 알아보겠습니다!
이 코드는 userService.ts
파일에 있는 사용자의 회원가입 로직을 나타냅니다. registerUser
함수는 사용자의 회원가입을 처리하는 기능을 담고 있습니다.
그렇다면 이 함수가 제대로 동작하는 지 어떻게 알 수 있을까요? 이런 경우 단위테스트를 통해 registerUser
함수가 제대로 동작하는 지 검증할 수 있습니다.
registerUser
함수에 대한 단위테스트를 설계할 때, 총 3가지 경우의 수를 생각해 볼 수 있습니다.
[성공]
1) 사용자가 회원가입을 성공적으로 완료한다.
[예외]
1) 이메일이 이미 존재할 때, EmailAlreadyExistsException 예외 메시지가 반환된다.
2) 닉네임이 이미 존재할 때, NicknameAlreadyExistsException 예외 메시지가 반환된다.
이 3가지 경우의 수를 통해 registerUser
함수가 올바르게 동작하는 지, 예외 상황에서 적절한 응답을 하는지를 검증할 수 있습니다. 이렇게 함수의 주요 기능과 예외 처리를 테스트 하면 코드의 안정성을 높이고 버그를 줄일 수 있습니다.
Test Code를 작성할 때 가장 먼저 고려해야 할 것은 Mocking입니다. 그렇다면 Mocking이 무엇이고 왜 중요한지 알아보겠습니다.
Mock이란 '가짜'입니다. 따라서 Mocking은 특정 부분을 실제가 아닌 '가짜'로 대체하는 과정이라 생각하시면 됩니다.
이 코드는 아까 봤던 코드의 전체입니다. registerUser를 테스트 한다면 이와 같은 생각을 할 수 있습니다.
위의 코드에서 registerUser
함수는 userRepository
와 userMapper
에 의존적입니다. 즉, registerUser
를 테스트 하려면 이 두 컴포넌트의 동작이 필요합니다.
테스트 환경에서 실제 userRepository
와 userMapper
인스턴스를 사용하면 여러 문제가 발생할 수 있습니다. 예를 들면, service 로직은 데이터베이스와 밀접하게 연결되어 있기 때문에 실제 데이터베이스에 접근해 테스트를 시 데이터베이스의 연결 오류나 데이터 변경 같은 문제가 발생할 수 있습니다.
또한, 실제 인스턴스를 사용할 때에는 테스트 속도가 저하되고 테스트 환경의 다양한 설정값들을 관리해야 합니다. 이러한 문제들을 방지하기 위해 Mocking을 사용하여 실제 동작을 모방한 '가짜' 컴포넌트를 테스트 환경에서 사용합니다.
NestJS에서 테스트 코드는 주로 .spec.ts
파일에 작성됩니다.
코드를 살펴보기 전에 간단한 흐름을 요약하자면, 먼저 mocking 할 컴포넌트들을 생성하고 beforeEach
에서 userRepository
와 userMapper
를 mocked 인스턴스로 생성하여 의존성을 주입합니다.
findOne
와 save
메서드를 mocking하기 위해 repository
를 mock으로 생성하였고, mapper
역시 마찬가지의 원리로 mock으로 생성했습니다.
findOne
과 save
는 jest.fn()
을 사용해 mock 함수로 생성되었습니다. 이렇게 mock 함수로 대체함으로써 테스트에서 실제 findOne
과 save
의 동작로직이 아닌 가상의 반환값이나 동작을 테스트할 수 있게 됩니다.
mockRepository
는 함수이기 때문에 호출될 때마다 새로운 mock 객체를 생성합니다. 이렇게 작성한 이유는 각 테스트 케이스마다 새로운 초기 상태의 mock 객체를 가지게 해 테스트 간의 독립적인 상태를 지니기 위해서 입니다.
MockRepository
타입과 MockMapper
타입을 생성했습니다.
MockRepository
타입은 Generic을 사용하여 주어진 객체의 모든 메서드를 mock 함수로 대체할 수 있는 타입을 정의했습니다.
MockRepository
타입은 다양한 객체에 적용 가능하기 때문에 다양한 repository의 타입에 대해 mock 객체를 일관된 방식으로 생성할 수 있습니다.
describe
내에서 미리 mockDto, mockLoginDto를 선언하면 각 test case를 작성할 때마다 재 선언할 필요가 없어져 재사용성이 향상됩니다.
beforeEach
는 각 test case들이 실행하기 전에 실행하는 코드입니다. module에 moked된 userRepository와 userMapper를 주입시킵니다.
그리고 나서, mocked 된 userRepository
와 userMapper
인스턴스를 생성합니다.
afterEach
는 test case들이 실행된 후 실행하는 코드입니다.
findOne
mock 함수를 초기화하기 위해 mockClear
를 사용합니다.