앞서 무엇을 테스트할 것인가? 에 대한 정리를 해보았다.
이번에는 어떻게 테스트할 것인가에 대해 정리를 이어보겠다.
"테스트 불가능한 영역을 Boundary Layer로 올려서 테스트 가능하도록 변경"
그렇다면 우리는 위의 상황에서 어떻게 코드를 구성해야 테스트 커버리지를 키울 수 있을까?
테스트할 수 없는 메소드를 Boundary 영역까지 끌어올리면 테스트할 수 있는 영역을 많이 확보할 수 있다.
예시로 이해해보자.
만약 로직 중 가장 내부에 있는 isValid()
메소드에서 LocalDate.now()
를 호출한다면 이를 감싸고 있는 전체 로직은 테스트하지 못하게 된다.
그렇기 때문에 LocalDate.now()
같은 특정 시간을 파라미터로 넘겨주는 형태로 외부로 빼내어 테스트 할 수 있는 커버리지를 넓혀야 한다.
이렇게 되면 이제는 어느 boundary까지 밖으로 빼내야 할지 고민이 생긴다.
설계나 사람마다 그 기준은 달라지겠지만 이 강연에서는 한 모듈로서의 의미를 지니는 가장 바깥쪽까지 뽑아내야 한다고 말하고 있다.
우리는 '우리가 spring을 쓴다고 꼭 @SpringBootTest
를 사용해야 할까?' 에 대한 고민을 해야한다.
다음 두 코드를 비교해보자.
@Autowired
private Member member;
private Member member;
단순히 @Autowired
어노테이션 차이밖에 없지만 위의 member
와는 다르게 아래의 member
는 null
객체가 된다.
이처럼 너무 spring context에만 의존하다보면 결국엔 Java개발자가 아닌 framework에만 의존하는 spring 개발자가 될 것이다.
TestDouble
"Non-Testable한 영역을 테스트해야할 때 사용"
1,2,3,4가 상호작용을 하는 아래 그림에서 4번 메소드를 테스트한다면 보통은 1,2,3 메소드를 TestDouble 처리할 것이다. 하지만 이렇게 되면 테스트가 구현 내용을 알아야 한다는 것을 의미한다.
만약 2의 반환 타입을 변경하거나 3의 입력 값을 변경하면 테스트가 실패하게 된다.
즉, TestDouble의 남용은 테스트를 구현 테스트로 유도할 가능성이 있다.
Boundary context까지 올라온 Non-Testable한 영역이 있을 때 이 영역에 대해 TestDouble 사용을 권장한다.
2, 3 영역도 각각의 의미를 가진다면 이 영역들 또한 테스트를 통해 검증된 영역이기 때문에 4영역이 테스트 될 때 함께 작동해도 좋다.
"편리하지만 사용을 의식하여 최소화!"
순수 자바 어플리케이션으로 테스트 할 수 없는 것이 있다.
그렇다면 이렇게 제어할 수 없는 영역은 어떻게 테스트 할 수 있을까?
바로 아래와 같은 Embedded를 활용하여 제어 불가능한 영역을 제어 가능하게 만드는 것이다.
단 너무 남용을 해서는 안되고, 테스트와 동일한 라이프 사이클을 갖도록 구성해야 한다.
아래는 이에 대한 예시이다.
그렇다면 임베디드가 로컬과 비교해 갖는 장점은 무엇이 있을까?
라이브러리 버전 등 호환 이슈 때문에 테스트의 정확도가 낮을 수 있지만 실행하는 속도가 빠르고 안정적이기 때문에 FIRST 원칙을 좀 더 잘 지킬 수 있어 로컬보다는 Embedded를 추천하고 있다.
스프링에서 지원하는 EndPoint Test는 아래와 같은 종류가 있다.
이러한 EndPoint Test를 작성할 때 요령을 알아보자.
응답, 요청 스펙을 검증하는 테스트를 작성할 때, 앞의 내용에 따라 테스트하려는 controller의 내부에는 Non-Testable한 영역이 하나도 없어야 하고 내부의 모든 비즈니스를 모두 알아야 한다.
하지만 이렇게 하려면 테스트 작성에 너무 많은 시간과 노력을 들여야 한다.
그래서 controller 뒤를 모르게 만들어 요청과 응답만 검증한다는 전제를 갖고 작성하는 현실적인 타협을 추천한다.
테스트를 진행할 때 RestDoc을 사용하면 테스트를 하며 문서화를 한번에 진행할 수 있다.
contract를 사용하여 설계에 맞는 내용을 작성 후 PR을 날리면
받은 쪽에서는 generateContractTests
를 수행하면 자동으로 테스트 내용이 작성된다. 이 내용은 jar파일로 만들어지기 때문에 추출하여 다른 곳에 공유가 가능하다.
모든 테스트의 순서와 관계를 고려하며 작성하기 어렵기 때문에 테스트는 상호 독립적으로 작성되어야 한다.
임베디드를 사용할 때도 마찬가지로 공유되는 자원은 초기화하여 다른 테스트에 영향을 주거나 받지 않도록 하는 것이 중요하다. 아래와 같이 @After
나 @AfterEach
등을 사용하여 테스트 종료 시 초기화하는 룰이나 습관을 갖는 것이 좋다.
만약 테스트가 순서대로 진행되기를 원한다면 JUnit5가 지원하는 DynamicTest
를 이용해 한 라이프사이클에서 테스트가 동작하도록 구성한다.
누군가 테스트만 보더라도 비즈니스 플로우를 파악할 수 있는 테스트가 좋은 테스트이다.
아래와 같은 형식이라면 그것을 파악하기까지 많은 시간이 들 수 있다.
즉, 테스트 코드도 가독성이 중요하다.
테스트의 가독성, 안정성, 요구사항 정리 등 비즈니스 코드와 동일한 수준의 리펙토링이 함께 이루어져야 한다.