좋은 테스트란 ?
일반적으로 좋은 테스트란?
테스트에 관한 기준은 상황 및 사람에 따라 다르겠지만 일반적으로 테스트 관련 책이나 문서들을 보면 아래와 같은 특성을 가진 테스트가 좋은 테스트라고 말합니다.
- 검사하고 싶은게 무엇인지 알기 쉬워야 한다.
- 테스트를 믿을 수 있어야 한다.
- 빨리 결과를 알 수 있어야 한다.
- 독립적으로 실행이 가능해야 한다.
- 특정 환경에 종속되지 않아야 한다.
테스트 코드를 짤 때, 특정 언어나 기술을 사용했다고 좋은 테스트야! 라고 말하지는 않습니다. 그 보다는 위와 같은 특성을 얼마나 가지고 있는지의 여부가 좋은 테스트인지 아닌지를 판가름하는 것 같습니다.
(물론 위에 더 많은 특성들이 존재하지만 일부만 적었습니다. 관련 내용이 궁금하시면 아래 참고자료를 봐주세요)
1. 테스트의 결과를 믿을 수 있어야 한다
테스트는 테스트의 이름만 보고 무엇을 테스트 하는 건지 명확하게 알 수 있어야 합니다. 그리고 믿을 수 있는 코드여야 합니다. 간단한 예를 들어 아래와 같은 코드가 있습니다.
테스트 이름은 할인이 10%일 때 제품 가격을 계산해라 입니다. 위의 테스트를 실행하면 통과 합니다. 하지만 저 코드의 속을 들여다 보면 테스트 이름과 다른 로직으로 계산을 합니다.
위의 테스트 코드에서는 하나의 단언문만 선언하여 통과시켰습니다. 이 결과를 맹신하고 추가로 다른 개발을 진행했다면? 이후에 "어디에서 계산이 잘못된거지?" 생각하며 모든 코드를 뒤져봐야 할 수도 있습니다...
위 테스트의 단언문은 주어진 값이 운 좋게 잘 맞아서 테스트를 통과했지만, 만약 다른 케이스를 하나만 더 추가했더라도 계산 로직이 잘못 됐다는 것을 파악하고 로직을 수정할 수 있습니다.
%로 계산한다고 생각하면 쿠폰이 20, 연필 200 일 때 가격은 160원이 되는 테스트 케이스를 추가해보면 실제로는 180원으로 계산이 되기 때문에 %로 계산을 안한다는 것을 발견할 수 있습니다.
위 테스트 코드는 이름을 잘못 짓고, 여러 테스트 케이스를 체크하지 않은 것이 믿을 수 없는 테스트 코드가 된 케이스 입니다. 불필요한 비용을 없애기 위해서라도 믿을 만한 테스트 코드를 만들어야 합니다.
- 테스트 코드 이름을 잘 지어야 한다
- 여러 테스트 케이스를 추가해야 한다 (가능하다면 예외상황도 고려해서)
2. 특정 환경에 종속되면 안된다
우선 예시 코드를 보자. 이 코드는 저장되어 있는 파일(결제 내역이 저장된)을 읽어서 어딘가(DB 이거나 또는 어떤 곳) 저장하는 코드를 테스트하는 예시입니다.
위와 같은 예시 코드는 Mac OS 에서는 잘 돌아가는 코드지만 윈도우에서는 테스트가 실패할 수 있습니다. 아래처럼 코드를 추가하면 윈도우에서 작동할 수 있습니다.
이제 Mac과 윈도우에서 돌아가는 코드가 됐지만 만약 또 다른 os 에서 동작할 일이 생긴다면? 또다시 if 문을 추가해야 합니다. 환경의 변화에 유연하게 대처하지 못하는 코드입니다. 만약 환경이 절대로 바뀔 일이 없다면 위와 같은 코드를 작성해도 문제될 것은 없지만 절대적인 것은 없습니다. 특정 환경에 종속되지 않고 예외상황을 반영한 코드를 작성하는 편이 좋습니다.
- 환경(시간, 임의성, 인프라, 네트워킹, os 등) 에 종속되는 코드를 작성하지 말자.
- 파일 시스템의 특정 경로를 명시적으로 참조하지 말자.
3. 독립적으로 실행이 가능해야 한다.
테스트 코드는 어떤 상황에서도 정상적으로 동작해야 합니다. 만약 데이터베이스에서 값을 조회하고 계산하여 리턴해주는 기능을 테스트하고 싶다고 할 때, 데이터베이스 연결이 끊어졌다면? 테스트를 못하는 걸까? 기능만을 검사하는 코드를 새로 만들어야 하는걸까요? 새로 만들 필요 없이 같은 타입의 대역을 세우면 됩니다.
물론 데이터베이스와 연결해야 하는 통합테스트는 필요합니다. 하지만 데이터베이스와 무관하게 계산 기능만을 검증하고 싶을 때가 있을 것 입니다. 데이터베이스(외부환경)와는 무관하게 독립적으로 실행이 가능한 테스트가 필요하다는 것 입니다. 아래와 같은 코드가 있습니다.
payService#calculateTotalPurchaseAmount() 메서드는 DB 를 조회하고 계산을 합니다. 그런데 DB를 조회할 수 없는 상황이 되었는데, 계산식을 검증하고 싶습니다. 그럴 땐 PayRepository 의 역할을 수행할 수 있는 payRepository 대역을 세워서 문제를 해결할 수 있습니다.
FakePayRepository 는 PayRepository 를 구현하고 있습니다. DB에 저장하고 읽는 기능을 Map 에 저장하고 읽는 기능으로 대체했습니다. 이렇게 하면 PayRepository 를 사용할 때와 FakePayRepository 를 사용할 때 같은 인터페이스를 사용할 수 있습니다. 같은 인터페이스를 사용하기에 기존의 PayRepository를 사용하는 PayService 의 코드는 변경할 필요없이 원래 테스트 하고자 했던 계산기능을 테스트 할 수 있습니다.
기존에 JPA 를 사용하든 MyBatis 를 사용하든 상관없이 오직 테스트에서만 사용하는 대역을 만든 것 입니다. 만약 MyBatis 를 사용하다가 JPA 로 변경한다고 해도 이 테스트 코드는 정상적으로 동작합니다. 주변환경과는 무관한 독립적인 테스트 코드를 만들었습니다!
- 어느 환경에서든 동일하게 동작하는 독립적인 테스트를 만들어야 합니다.
- DB 와의 연결을 끊어 테스트를 빠르게 실행할 수 있습니다.
추가로 대역을 사용하면 세부적인 구현을 뒤로 미룰 수 있습니다. 만약 설계 당시에 JPA 를 사용할 지 MyBatis 를 사용할지 아직 결정이 안 났다고 하면. 어떤 방식을 사용할 지는 뒤로 미루고 계산 로직(비즈니스 로직)에만 집중해서 코딩할 수 있습니다. 세부적인 구현(쿼리)을 작성하면서 (테스트)코드를 작성하는 방식에서 벗어나 먼저 비즈니스 로직에 집중해서 (테스트)코드를 작성하고 이후에 세부적인 구현(쿼리작성)을 할 수 있습니다. 이러한 코딩 방식 및 대역에 대해 궁금하시면 아래 참고에 나온 책의 TDD에 잘 나와 있습니다!
정리
좋은 테스트 코드가 있을 때 좋았던 점은 리팩토링을 수월하게 할 수 있다는 것 입니다. 리팩토링은 기능은 동일하게 동작하게 하면서 코드를 정리하는 작업을 말합니다. 내부 코드를 변경하는 작업이기에 코드의 변경이 전체 기능에 어떻게 영향을 끼치는지 체크를 해야 합니다. 그럴 때 좋은 테스트 코드가 있다면? 테스트 코드를 실행했을 때 실패한다면 리팩토링의 결과가 기존 기능에 영향을 끼쳤다는 것을 알 수 있습니다. 테스트 코드를 이용해 리팩토링 하는 방법은 다음 포스팅에서 다뤄보겠습니다.
참고
- 테스트 주도 개발 시작하기 (최범균) - 가메출판사
- JUnit In Action - Manning 출판사
- Effective Unit Testing - Manning 출판사