기초가 탄탄한 자바 개발자가 되기 위해 알아야할 테스트 기초 (testing fundamentals)
원문 : Well Grounded Java Developer - ch13 testing fundamentals
이 장에서 다루는 것
- 테스트를 하는 이유
- 테스트 하는 방법
- 테스트 주도 개발
- 테스트 대역
- JUnit 4에서 5로 migration
이 글에서 전체를 다루지는 않고 13.2 까지 하여 테스트를 하는 이유와 테스트 하는 방법에 대한 부분까지 다룬다.
최근 몇 년 사이 프로그래밍 분야에서 테스트 자동화를 개발 과정의 당연한 부분으로 받아들이게 되었습니다. 테스트는 시스템과 동작을 보장하기 위해서 개발자의 로컬 환경과 지속적 통합(CI, continuous integration) 환경에서 수행됩니다. 테스트에 대한 다양한 도구, 접근 방식, 철학들도 폭발적으로 나오고 있습니다.
모든 기술이 그러하듯 만능인 것은 없습니다. 모든 상황을 다 테스트를 할 수는 없습니다. 그렇기 때문에 왜 테스팅을 해야하는지에 대해 이해하는 것이 중요합니다. 그래야 어떻게 테스트를 할 지에 대해서 더 좋은 결정을 할 수 있거든요.
13.1 테스트 하는 이유
사실 테스트 를 하는 이유는 매우 다양합니다. 대략적으로 다음과 같습니다.
- 각각의 메소드의 로직이 올바른지 검증
- 코드로 작성된 두 객체의 상호작용에 대한 검증
- 라이브러리나 외부 의존성이 기대한대로 동작하는지 검증
- 시스템에서 생산되거나 소비되는 데이터가 올바른지 검증
- 시스템이 외부 컴포넌트와 적절하게 동작하는지 검증
- end to end 로 시스템의 핵심 비지니스 시다나리오 검증
- 이후로 코드를 볼 사람을 위한 가정들을 문서화 (주석 및 문서와는 별개로 존재)
- 객체들의 밀접한 결합 여부와 책임들을 발견하게 도와줘 시스템 디자인에 영향을 줍니다.
- 사람이 수행해야했던 체크리스트 작성 자동화
- 무작위 입력을 통해 코드에서 예상치 못한 코너 케이스 찾기
목록을 읽어보면 알겠지만 코드를 테스트한다는 것이 그렇게 간단하지는 않습니다. 따라서 우리는 테스트를 할 때 스스로에게 다음과 같이 질문을 해야합니다.
- 이 코드를 테스트하려는 이유는 무엇인가
- 그 목표를 정확하고 깔끔하게 달성할 수 있는 방법은 무엇인가
엣지 케이스와 코너 케이스
엣지 케이스 : 매개변수의 값이 극단적인(최대 또는 최소) 상황에서 발생하는 문제 또는 상황
코너 케이스 : 매개변수가 지정된 범위 내에 있더라도 여러 환경 변수 또는 조건이 동시에 극단적인 수준에 있을 때 나타나는 문제 또는 상황
13.2 테스트 하는 방법
다양한 유형의 테스트에 대해서 논의할 때 가장 대표적으로 사용되는 도구는 테스트 피라미드 입니다. (Mike Cohn의 Succeeding with Agile에서 처음으로 나옴)
테스트 피라미드는 테스트 유형에 따른 비용의 균형에 대해서 설명합니다. 유형의 정확한 경계에 대해서는 여전히 논쟁이 벌어지고 있지만 핵심 아이디어는 매우 유용합니다.
[Note] 테스트 유형은 테스트 도구에 결정되는 것은 아닙니다. Junit을 사용한다고 해서 단위 테스트를 작성하는 것이 아니며 명세 라이브러리 (specification library)를 사용한다 해서 이해하기 쉬운 인수 테스트를 생성한다고는 보장하지 않습니다. 테스트 유형은 우리가 어떤 부분을 테스트하고 증명하려는지에 대한 것 입니다.
명세 라이브러리가 무엇을 이야기 하는건지 모르겠어서 ChatGPT에게 물어보니 JBehave, Cucumber 를 예시로 들었다. (BDD 도구들을 명세 라이브러리로 보는 것 같다.) JBehave의 공식 홈페이지에서는 BDD를 “BDD is an evolution of test-driven development (TDD) and acceptance-test driven design” 이라고 소개하고 있긴 하다.
BDD 에 대한 것은 이전에 BDD를 소개합니다. 라는 글을 통해 정리해두었다.
단위 테스트는 피라미드의 아래 부분에 있습니다. 단위 테스트는 시스템의 한 부분에만 집중하는 테스트입니다. 한 측면이라는 것은 어떻게 정할 수 있을까요? 외부 종속성과 어떻게 관련되는지를 보면 알 수 있습니다. 예를들어 테스트가 결과에 대한 일부 논리를 수행하기 전에 데이터베이스를 호출했다면 이는 더 이상 하나를 테스트하는 것이 아닙니다. (데이터베이스 검색에 대한 테스트와 로직의 동작여부에 대한 테스트를 수행해야 합니다.) 이러한 외부 종속성은 일반적으로 네트워크 서비스나 파일도 포함합니다.
한 부분에 집중하는 원칙을 위반하는 것을 피하기 위해 일반적으로 테스트 대역을 사용합니다. 예를 들면 단위 테스트 안에서 실제 데이터베이스를 사용하지 않고 페이크 객체를 사용하는 것입니다. 이 부분에 대해서는 다음 섹션에서 더 자세히 알아보겠습니다. 그러나 기본 아이디어는 이 대역들은 다양한 특징을 가지고 있으며 잘 사용하기 위해서는 많은 사항을 고려해야 한다는 것입니다.
단위 테스트는 여러가지 매력적인 점들이 있습니다. 그로인해 테스트 피라미드에서 가장 큰 부분을 차지하고 있습니다. 이유를 설명하자면 다음과 같습니다.
- 빠름 : 외부 종속성이 없다면, 실행하는데 시간이 오래 걸리지 않습니다.
- 집중 : 코드의 단위(unit)에 대해서만 다루기 때문에, 큰 테스트를 할 때보다 훨씬 명확합니다.
- 신뢰성있는 실패 : 외부 상태에 대한 외부 종속성을 최소화 하였기 때문에 더 결정적입니다.
좋아 보이지만, 그렇다고 단위 테스트만 작성하는건 좋지 않습니다. 단위 테스트가 모든 규모의 코드에 적합한 것은 아니기 때문에 다음의 문제들이 발생할 수 있습니다.
- 밀접한 결합 : 단위 테스트는 구현과 밀접하게 관련되어 있습니다. 구현 세부 사항에 너무 밀접하게 결합되는 경향이 있습니다. 구현이 변경되었을 때 테스트가 유효하지 않게 되기 쉽습니다.
- 의미 있는 상호작용의 부재 : 코드를 단위로 나누어 보는 것은 매력적일 수 있지만, 프로그램의 실제 작업은 의존적인 부분 간의 상호 작용을 포함하고 있고, 단위 테스트에서는 이를 놓칠 수 있습니다.
- 내부 중심 : 테스트의 목표는 최종 사용자가 올바른 결과를 얻을 수 있도록 하는 것입니다. 한 메소드의 정확성이 실제 유저의 기대하는 결과로 이어지는 경우는 드뭅니다.
테스트 피라미드의 다음 단계인 통합 테스트 는 유닛 테스트의 종속성 제한에서 벗어난 테스트입니다. 통합테스트는 경계를 넘어 시스템의 다양한 부분들이 원활하게 통합되도록 하는데 중점을 둡니다.
단위 테스트와 마찬가지로 통합 테스트도 시스템의 일부만 선택하여 실행할 수 있습니다. 외부 서비스와 같은 일부 종속성은 여전히 테스트 대역으로 대체하되, 데이터베이스와 같은 종속성은 테스트에 포함시킵니다.
이 부분은 8장 통합 테스트를 하는 이유 (2) - 언제 목을 써야할까? + 예시 글의 관리 의존성과 비관리 의존성 부분을 참고하면 좋을 것 같다.
중요한 점은 단위를 넘어섰다는 것입니다.
단위 테스트와 통합 테스트의 경계는 모호할 수 있습니다. 하지만 명확하게 통합 테스트인 예시를 들자면 다음과 같습니다.
- 데이터베이스가 필요하고, 데이터 접근 코드를 호출해야 하는 테스트
- 특수한 프로세스 내부의 HTTP 서버를 동작하여 이에 대한 실제 요청을 하는 테스트
- (테스트 환경이든 아니든) 다른 서비스에 실제 요청을 하는 경우
통합 서비스는 다음과 같은 장점이 있습니다.
- 넓은 커버리지 : 통합 테스트는 더 많은 코드와 작업을 수행할 수 밖에 없습니다.
- 추가 검증 : 특정 유형의 에러는 실제 종속성이 있을 때만 확인할 수 있습니다. 예를 들어, SQL문의 구문 오류는 실제 데이터베이스를 호출하지 않으면 찾기가 어렵습니다.
물론 모든것에는 트레이드오프가 있습니다. 통합 테스트는 잘 관리되지 않으면 다음과 같은 이유로 오히려 문제가 되기도 합니다.
- 느린 테스트 : 메모리에서 값을 읽는 대신 실제 데이터베이스를 사용하는것은 훨씬 느립니다. 그걸 여러 테스트에서 계속 반복해서 수행한다면 수행 시간이 길어지게 됩니다.
- 비결정적인 결과 : 외부 종속성은 테스트 중간에 상태가 변경될 가능성이 있습니다. 예를들어 데이터베이스의 남아있는 레코드로 인해 SQL문에서 반환되야 하는 데이터가 변경될 수 있습니다.
- 잘못된 확신 : 통합 테스트는 메인 시스템과 미묘하게 다를 수 있습니다. 예를들어 테스트 환경과 프로덕션 환경에서 데이터베이스 버전이 다를 수 있습니다. 이럴경우 통합 테스트는 모든 것이 정상인 것처럼 잘못 나타날 수 있습니다.
이러한 부분들에도 불구하고 통합 테스트는 테스트 영역에서 중요한 부분입니다.
엔드 투 엔드 (End to End) 테스트 는 통합 테스트를 넘어 시스템에 대한 사용자 경험 전체를 복제하는 것을 목표로 합니다. 웹 브라우저나, 기타 어플리케이션을 프로그래밍을 통해 제어하거나, 테스트 환경에서 완전히 배포된 서비스 인스턴스를 활용하는 것을 의미할 수 있습니다. 엔드 투 엔드 테스트는 다음과 같은 이점을 가집니다.
- 진짜 사용자 경험 : 좋은 엔드 투 엔드 테스트는 사용자가 보는 것과 유사합니다. 이를 통해 사용자의 고수준 기대를 직접 확인할 수 있습니다.
- 진짜 환경 : 많은 엔드 투 엔드 테스트는 테스트, 스테이징, 프로덕션 환경에서 실행됩니다. 코드가 잘 동작하도록 관리된 환경 외에서도 동작하는지 확인합니다.
- UI 사용 가능 : 많은 엔드 투 엔드 테스트는 웹 브라우저를 제어하는 것과 같은 방식을 사용하여 시스템 측면에서 볼 수 있습니다. (e.g. 버튼이 렌더링 되면 이후에 클릭)
그러나 엔드 투 엔드 테스트에는 다음과 같은 어려움도 있습니다.
- 더 느린 테스트 : 일반적으로 유닛 테스트는 즉시 완료되고 통합 테스트는 1초 미만입니다. 그러나 엔드 투 엔드 테스트는 웹 브라우저를 제어해야 하기 때문에 훨씬 더 많은 시간이 소요됩니다.
- 불안정한 테스트 : 일반적으로 E2E 테스트 도구들은 (특히 UI를 다루는 E2E 테스트 도구는) 불필요한 실패를 피하기 위해 재시도와 긴 timeout을 가지고 있습니다.
- 취약한 테스트 : 엔드투 엔드 테스트는 피라미드의 윗 부분에 있기 때문에, 아래 부분에서 수정이 발생된다면 실패할 가능성이 높습니다. 별거 아닌것 같은 텍스트 변경으로도 의도치 않게 테스트가 중단될 수 있습니다.
- 더 어려운 디버깅 : 엔드 투 엔드 테스트에서는 테스트를 제어하기 위해 보통 다른 층(Layer)를 사용합니다. 이는 무엇이 잘못되었는지 알아내는데 더 복잡하게 합니다.
그러면 각 테스트 유형 간의 올바른 테스트 비율은 어떻게 될까요? 사실 정해진 답변은 없습니다. 각 프로젝트와 시스템의 요구에 다라 다릅니다. 하지만 피라미드는 시스템의 각 부분을 어떻게 테스트할지 장단점을 고려하는데 도움이 될 수 있습니다.
유일한 방법은 아니겠지만, 기초가 탄탄한(well-grounded) 개발자는 시스템이 커짐에 따라 테스트 수준을 명확하게 유지하는데 테스트 주도 개발이 도움이 된다는 것을 발견할 것입니다.
다음 글에서는 테스트 주도 개발에 대해서 이어서 정리한다.