지난 시간에 이어 TDD 책을 읽고 공부한 내용을 정리해보려고 한다.
테스트 주도 개발 시작하기 - 최범균 저
테스트 가능한 설계
테스트가 어려운 코드
- 하드 코딩된 경로
- ex) 파일 경로, 환경 변수, IP 주소, 포트 번호 등을 하드 코딩
- 의존 객체를 직접 생성
- 의존 객체를 직접 생성한다면 이 객체가 올바르게 동작하는데 필요한 모든 환경을 구성해줘야 한다.
- 정적 메서드 사용
- 실행 시점에 따라 달라지는 결과
- ex) 현재 시간, 난수 등을 테스트에 이용하는 경우
- 역할이 섞여 있는 코드
테스트 가능한 설계로 전환
- 하드 코딩된 상수를 생성자나 메서드 파라미터로 받기
- 의존 대상을 주입 받기
- 의존 대상은 주입 받을 수 있는 수단을 제공해서 교체할 수 있도록 한다.(생성자 주입)
- 의존 대상을 교체할 수 있게 되면 실제 구현 대신에 대역을 사용할 수 있어 테스트가 원활해진다.
- 테스트하고 싶은 코드 분리하기
- 기능의 일부만 테스트하고 싶다면 해당 코드를 별도 기능으로 분리해서 테스트할 수 있다.
- 시간이나 임의 값 생성 기능 분리하기
- 테스트 대상이 사용하는 시간이나 임의 값을 제공하는 기능을 별도로 분리해서 테스트 가능성을 높일 수 있다.
- 외부 라이브러리는 직접 사용하지 말고 감싸서 사용하기
- 외부 라이브러리가 정적 메서드를 제공한다면 대체하기 어렵다.
- 외부 라이브러리와 연동하기 위한 타입을 따로 만들고, 테스트 대상은 분리한 타입을 사용하게 바꾼다.
- 테스트 대상 코드는 새로 분리한 타입을 사용함으로써 외부 연동이 필요한 기능을 쉽게 대역으로 대체할 수 있다.
테스트 범위와 종류
테스트 범위에 따른 테스트 종류에는 기능 테스트, 통합 테스트, 단위 테스트 세 가지가 있다.
기능 테스트(Functional Testing)
- 사용자 입장에서 시스템이 제공하는 기능이 올바르게 동작하는지 확인한다.
- 테스트를 수행하기 위해 시스템을 구동하고 사용하는데 필요한 모든 구성 요소(웹 브라우저, DB, 웹 서버, 외부 서비스 등)들을 하나로 엮어서 진행한다.
- 끝에서 끝까지 모든 구성 요소를 논리적으로 완전한 하나의 기능으로 다루고 검사하기 때문에 E2E(End to End) 테스트로도 볼 수 있다.
- QA 조직에서 수행하는 테스트가 주로 기능 테스트이다.
통합 테스트(Integration Testing)
- 시스템의 각 구성 요소가 올바르게 연동되는지 확인한다.
- 기능 테스트가 사용자 입장에서 테스트하는 데 반해 통합 테스트는 소프트웨어의 코드를 직접 테스트한다.
- ex) 기능 테스트 : 앱을 통해 가입 기능 테스트 / 통합 테스트 : 서버의 회원 가입 코드를 직접 테스트
- 테스트 대상 : 프레임워크, 라이브러리, 데이터베이스, 구현한 코드 등
단위 테스트(Unit Testing)
- 개별 코드나 컴포넌트가 기대한대로 동작하는지 확인한다.
- 한 클래스나 메서드와 같은 작은 범위를 테스트
- 일부 의존 대상은 대역으로 대체
테스트 범위에 따른 테스트 코드 개수와 시간
- 기능 테스트를 수행하려면 클라이언트부터 DB까지 모든 환경이 갖춰져야 하기에 다양한 상황별로 테스트하기 가장 어렵다. 때문에 기능 테스트는 정상적인 경우와 몇 가지 특수한 상황만 테스트 범위로 잡는다.
- 통합 테스트는 기능 테스트에 비해 제약이 덜하다. 또한, 내부 구성 요소에 대한 테스트도 가능하다. 통합 테스트는 기능 테스트에 비해 상대적으로 실행 시간이 짧고 상황을 보다 유연하게 구성할 수 있기 때문에 기능 테스트보다 통합 테스트를 더 많이 작성한다.
- 단위 테스트는 통합 테스트로도 만들기 힘든 상황을 쉽게 구성할 수 있다. 더 작은 단위를 대상으로 테스트 코드를 만들고 더 다양한 상황을 다루기 때문에 통합 테스트보다 단위 테스트 코드를 더 많이 작성하게 된다.
테스트 속도는 기능 테스트가 가장 느리고 다음으로 통합 테스트, 단위 테스트가 가장 빠르다. 그래서 가능하면 단위 테스트에서 다양한 상황을 다루고 통합 테스트나 기능테스트에서는 주요 상황에 초점을 맞춰야 한다. 그래야 테스트 실행 시간이 증가해 피드백이 느려지는 것을 방지할 수 있다.
테스트 실행 속도가 느려지면 테스트를 작성하지 않거나 테스트 실행을 생략하는 상황이 벌어진다. 이는 결국 소프트웨어의 품질 저하로 이어질 수 있기 때문에 가능하면 빠른 시간 내에 테스트를 실행할 수 있도록 해야 한다.
테스트 코드와 유지보수
TDD를 하는 과정에서 작성한 테스트 코드는 CI/CD에서 자동화 테스트로 사용되어 버그가 배포되는 것을 막아주고 이는 소프트웨어 품질이 저하되는것을 방지한다. 테스트 코드 또한 유지보수 대상이기 때문에 방치하게 되면 다음과 같은 문제가 발생할 수 있다.
- 실패한 테스트가 새로 발생해도 무감각해진다. 테스트 실패 여부에 상관없이 빌드하고 배포하기 시작한다.
- 빌드를 통과시키기 위해 실패한 테스트를 주석 처리하고 실패한 테스트는 고치지 않는다.
- 결국, 테스트 자체를 실행하지 않게 되고, 이는 소프트웨어의 품질 저하로 연결된다.
깨진 테스트가 발견되면 즉시 수정해서 테스트 실패가 확산되는 것을 방지해야 한다.
테스트 코드 유지보수를 위한 주의 사항
변수나 필드를 사용해서 기댓값을 표현하지 않기
- 기대값에 get method를 사용하는 것 보다 명확하게 상수를 사용하는 것이 가독성이 더 좋을 수 있다.
두 개 이상 검증하지 않기
- 한 테스트 메서드에 많은 단언을 하려고 하다가 서로 다른 검증을 섞는 경우가 있다. 물론 테스트 메서드가 반드시 한 가지만 검증해야 하는 것은 아니지만, 검증 대상이 명확하게 구분된다면 테스트 메서드도 구분하는 것이 유지 보수에 유리하다.
정확하게 일치하는 값으로 모의 객체 설정하지 않기
- 한정된 값에 일치하도록 모의 객체를 사용하면 약간의 코드 수정만으로도 테스트가 실패하기 때문에 모의 객체는 가능한 범용적인 값을 사용해서 기술해야 한다. ex) "pw" -> Mockito.anyString()
과도하게 구현 검증하지 않기
- 테스트 코드를 작성할 때 주의할 점은 테스트 대상의 내부 구현을 검증하는 것이다. 모의 객체를 처음 사용할 때 특히 이런 유혹에 빠지기 쉽다. 하지만 이는 테스트 코드 유지 보수에 도움이 되지 않는다. * 만약, 테스트 대상에서 A 메서드가 호출되었는지 또는 B 메서드가 호출되었는지 검증하게 되면, 구현이 조금만 변경되어도 테스트가 깨질 가능성이 커진다는 것이다.
- 내부 구현은 언제든지 바뀔 수 있기 때문에 테스트 코드는 내부 구현보다 실행 결과를 검증해야 한다.
셋업을 이용해서 중복된 상황을 설정하지 않기
- 테스트 코드를 작성하다 보면 각 테스트 코드에서 동일한 상황이 필요할 때가 있다. 이 경우 중복된 코드를 제거하기 위해 @BeforeEach 메서드를 이용해서 상황을 구성할 수 있다. 중복을 제거하고 코드 길이도 짧아져서 코드 품질이 좋아졌다고 생각할 수 있지만, 테스트 코드에서는 상황이 달라진다.
- 나중에 다시 보면 테스트 케이스가 한 눈에 들어오지 않고 가독성이 떨어진다.
- 모든 테스트 메서드가 동일한 상황을 공유하기 때문에 조금만 내용을 변경해도 테스트가 깨질 수 있다.
통합 테스트에서 데이터 공유 주의하기
- 통합 테스트 시, 두 가지로 초기화 데이터를 나누어 생각해야 한다.
- 모든 테스트가 같은 값을 사용하는 데이터 ex) 코드값 데이터
- 특정 테스트에서만 필요한 데이터 ex) 중복 ID 검사를 위한 회원 데이터
- 모든 테스트가 같은 값을 사용하는 데이터는 공유해도 된다.
- 특정 테스트에서만 의미 있는 데이터는 공유하지 말고 특정 테스트에서만 생성하는 편이 좋다.
실행 환경이 다르다고 실패하지 않기
- 로컬 서버 - 배포 서버, 운영체제 등에 따라 테스트가 다르게 작동하면 안 된다.
실행 시점이 다르다고 실패하지 않기
- 시간이 관련된 테스트를 하는 경우, 별도의 시간 클래스를 작성하여 테스트 코드의 시간을 원하는 시점으로 제어한다.
랜덤하게 실패하지 않기
- 랜덤하게 생성한 값이 검증에 영향을 준다면 구조를 변경해야 테스트가 가능하다.
- 직접 랜덤 값을 생성하지 말고 생성자를 통해 값을 받도록 수정하거나 랜덤 값 생성을 다른 객체에 위임
조건부로 검증하지 않기
- 테스트는 성공 or 실패
- 조건부로 단언을 실행한다면 테스트가 성공 또는 실패가 아닌 그대로 끝나는 경우가 있다.
- 조건에 대한 단언도 실행하여 반드시 성공 or 실패하도록 작성해야 한다.
통합 테스트는 필요하지 않은 범위까지 연동하지 않기
- 통합 테스트 실행 시 전체 애플리케이션을 구동하면 필요 없는 객체까지 생성하게 되어 테스트 속도가 느려진다.
- 테스트 대상 이외의 요소 때문에 실패할 수도 있다.
- 테스트에 필요한 부분만 연동하도록 해서 테스트 수행 시간을 줄이자.