이 글은 KSUG 최한뫼님이 번역하신 블로그를 정리한 것입니다. 원본은 링크를 참조해주세요.
비용을 천문학적으로 증가시킴에도 불구하고 프로젝트에 정말 아무런 도움이 되지도 않는 단위 테스트를 작성하기란 정말 쉽다.
단위 테스트는 버그를 찾기 위한 것이 아니다.
단위 테스트의 의도를 정확히 이해하는 것이 중요하다. 단위 테스트는 단순히 버그를 찾기 위한 효과적인 방법이 아니다.
단위 테스트는 시스템의 각각의 단위들을 개별적으로 조사하는 것이다. 시스템이 구현되어 실제 환경에서 동작할 때 모든 단위들은 완벽하게 하나의 유기체로 동작해야 한다.
단위 테스트가 독립적으로 잘 작동한다고 전체 기능이 잘 작동하는 것이 아니다. 따라서 단순히 버그를 찾기 위한 것이라면 통합 테스트에서 진행하는 것이 효과적이다. 단위 테스트는 TDD처럼 시스템 디자인 단계의 일부분으로 보아야 한다.
하나의 테스트 케이스는 단위 기능 중 하나의 시나리오만 테스트해야 한다.
단위 테스트 작성 시 가장 중요하게 인식할 점은 테스트 단위가 여러 개의 세트 시나리오들을 가질 수 있다는 것이다.
모든 테스트 시나리오들은 독립적인 테스트 코드로 작성되어야 한다. 예를 들어 매개변수를 가지고 처리한 후 값을 돌려주는 함수의 테스트 케이스를 작성한다고 해보자
- 첫 번째 파라미터가 Null일 경우 예외 객체를 반환해야 한다.
- 두 번째 파라미터가 Null일 경우 예외 객체를 반환해야 한다.
- 두 개의 파라미터가 모두 Null일 경우 예외 객체를 반환해야 한다.
- 파라미터가 정상 범위 안일 경우 작업 실행 후 결과 값을 반환해야 한다.
이렇게 세분화된 테스트 케이스들은 코드를 수정하거나 리팩토링시 효과적이다. 왜냐하면 단위 테스트만으로 수행하면 코드를 수정했을 때 코드의 기능을 망가뜨렸는지 확인할 수 있기 때문이다. 또한 기능을 수정한 후 최소한의 테스트 코드만 수정하면 된다.
불필요한 검증 구문을 작성하지 말자
단위 테스트는 시스템의 특정 단위가 어떻게 동작하는지 확인하는 것이지 단순히 단위 내의 코드가 모든 과정을 관찰하는 것이 아니다.
단위의 모든 부분에 대해 검증 구문을 작성할 필요가 없다. 대신 테스트하려고 하는 하나의 시나리오에 집중하자.
이렇게 작성하지 않으면 하나의 이유로 여러 테스트 케이스가 실패할 수 있다. 이렇게 되면 테스트 코드를 보고 어디서 문제가 발생했는지 찾기 어렵다.
각 테스트는 독립적이어야 한다.
다른 테스트에 의존하는 단위 테스트는 작성해서는 안된다. 이러한 테스트는 근본적인 실패 원인을 찾기 어렵다. 결국 별도의 디버깅 작업을 수행하게 만든다.
또한 상호 의존적인 테스트 코드는 유지보수도 번거롭다. 하나의 테스트 코드를 수정할 경우 의존성을 가지고 있는 다른 코드도 수정해야 할 경우가 생기기 때문이다.
테스트의 선결 조건을 설정하기 위해서는 @Before, @After 애너테이션을 사용하자. 만약 서로 다른 테스트를 위해 @Before, @Aftoer 애너테이션에서 여러 가지 세팅을 해야 한다면 별도의 새로운 테스트 클래스를 생성하는 것을 고려하자
모든 외부 서비스와 상태들에 테스트 더블을 사용하자
테스트 더블을 사용하지 않으면 공통된 외부 조건을 사용하는 테스트 구문들의 결과가 서로에게 영향을 미치게 된다. 결국 테스트 실행 순서에 따라 테스트 결과가 달라지거나 네트워크, 데이터베이스 조건에 따라 결과가 달라진다.
외부 서비스의 버그들로 인해서 테스트 결과가 실패하게 될 수도 있다. (테스트 구문이 정적 변수들을 변화게 해서도 안된다. 만약 변화시켜야 한다면 테스트 전에 변수들을 초기화시켜라)
단위 테스트 케이스의 이름은 명확하고 일관되며 테스트의 의도를 잘 나타내야 한다.
테스트 케이스의 이름은 테스트의 의도가 무엇인지 잘 반영해야 한다. 단순하게 테스트하려는 단위의 클래스와 메서드의 조합을 테스트 케이스의 이름으로 사용하는 것은 좋지 않다. 이런 방식은 클래스나 메서드의 이름이 변경되면 테스트 케이스의 이름도 매번 수정해줘야 한다.
그러나 단위 테스트 케이스의 이름이 단위의 기능을 반영하는 논리적인 이름이라면 단위 기능이 바뀌지 않은 경우에는 테스트 케이스 이름은 언제나 동일하다
아래는 테스트 케이스 이름의 좋은 예들이다.
- TestCreateMember_Null_Id_ShouldThrowException
- TestCreateMember_Negative_Id_ShouldThrowException
- TestCreateMember_Duplicate_Id_ShouldThrowException
- TestCreateMember_Valid_Id_ShouldPass
외부 시스템이나 서비스에 대한 의존성이 가장 낮은 메서드들에 대해 테스트를 먼저 작성하고 확장해라
예를 들어 Member 클래스를 테스트한다고 하자. 가장 먼저 Member 클래스를 생성하는 코드부터 테스트한다. 왜냐하면 Member를 생성하는 시나리오가 가장 낮은 외부 의존성을 가지고 있기 때문이다. 이 시나리오가 성공한다면, DB에 접근하는 테스트 코드를 추가하자
DB에 Member 정보를 가지려면 먼저 Member를 생성하는 테스트 시나리오를 통과해야 한다. 만약 Member 생성하는 코드에 버그가 있다면 훨씬 빨리 발견할 수 있다.
Private 메서드를 포함한 모든 메서드들은 가시범위에 상관없이 적절한 단위 테스트를 작성해야 한다.
private 메서드들이라도 반드시 테스트해야 한다. private 메서드는 중요한 역할을 하기 때문에 의도대로 동작하는 것을 확이해야 한다.
예상된 예외를 테스트하는 단위 테스트 코드를 작성하자
예외 처리가 잘되는지 테스트해야 할 때가 있다. 이때 try/catch문으로 검증하려고 하는 것은 좋지 않다.
대신 JUnit이 제공하는 방법을 사용하자
@Test(expected = BusinessLogicException.class)
가장 적합한 검증 구문을 사용하자
각 테스트 케이스에 사용할 수 있는 많은 검증 구문이 있을 수 있다. 각 경우마다 이유와 의도에 적합한 검증 구문을 사용하자
검증 구문 파라미터들은 적합한 순서대로 배치하자
일반적으로 검증 구문은 두 개의 파라미터를 가진다. 첫 번째는 테스트 결과가 패스할 때 기대하는 정상 값이고, 두 번째는 테스트 실제 결과 값이다. 이 두 값들을 순서대로 작성하자. 이렇게 하면 테스트 실패 시 에러 메시지를 통해 무엇이 잘못되었는지 쉽게 확인할 수 있다.
assertThat(exepectedData).isEqualTo(actualData);
테스트 코드 내에서 출력하지 말자
테스트 케이스가 제대로 작성되었다면 별도의 출력문이 필요하지 않다. 만약 출력문에 대한 필요성을 느낀다면 테스트 코드를 재검증해봐야 한다.
정적 변수를 테스트 클래스에 사용하지 마라. 만약 사용했다면 각 테스트 케이스 실행시마다 초기화해라
각 테스트들은 독립적이어야 한다. 따라서 정적 변수들을 사용하지 않아야 한다. 만약 사용해야 한다면 각 테스트 케이스마다 초기화시켜줘야 한다.
간접적인 테스트들에 의존하지 말자
하나의 테스트가 의도된 시나리오 외 또 다른 시나리오도 테스트한다고 가정하면 안 된다. 이 방식은 테스트 케이스에 혼란을 가져온다. 또 다른 시나리오를 테스트해야 한다면 추가적인 테스트 케이스를 작성하자
단위 테스트를 자동으로 실행하게 빌드 스크립트를 작성해라
테스트 케이스들이 빌드 스크립트를 통해 자동적으로 실행되게 해라. 이렇게 하면 테스트 실행환경과 애플리케이션의 신뢰성을 높여준다.
쓸데없는 테스트 케이스는 삭제해라
만약 단위 테스트의 코드가 적절하지 않다면 삭제하는 것이 좋다. @Ignore 애너테이션을 사용해서 불필요한 테스트 케이스를 포함하는 것은 별로 좋지 않다.
마무리
단위 테스트를 작성하는 것은 효율적이고 코드의 품질을 높여줄 수 있다. 하지만 잘못된 단위 테스트는 오히려 독이 된다. 단위 테스트를 작성하는 데에는 많은 비용이 들기 때문이다. 많은 비용이 들어가는 만큼 단위 테스트의 품질은 좋아야 한다.
이번에 새로운 프로젝트를 진행하면서 단위 테스트와 통합 테스트를 작성하면서 개발을 하고 있다. 어렵고 정말 시간이 많이 들어간다. 하지만 그만큼 단단한 프로젝트를 만들고 있다는 게 느껴진다.
좋은 테스트 코드란 무엇인지 이해하고 공부하면서 계속해서 테스트 코드를 발전시켜 봐야겠다.
'Spring' 카테고리의 다른 글
Spring - Spring Boot 초기 데이터 설정 (data.sql) (0) | 2023.05.29 |
---|---|
Spring - 통합 테스트에서 S3 Mock 객체로 S3 자원 아끼기 (2) | 2023.05.24 |
Spring - 이메일 인증 구현해보기 (랜덤 인증번호 보내기) (9) | 2023.04.21 |
Spring - 로컬 환경을 위한 Embedded Redis 적용하기 (+ Can't start redis server. Check logs for details) (5) | 2023.04.15 |
Spring - Redis를 사용해보자 (2) | 2023.04.15 |