좋은 단위 테스트의 4대 요소

좋은 단위 테스트의 4대 요소 자세히 살펴보기

좋은 단위 테스트의 4가지 특성

회귀 방지
리펙터링 내성
빠른 피드백
유지 보수성

회귀 방지

회귀 : 소프트웨어 버그

자신이 작성한 코드는 자산이 아니라 책임이다. 따라서 코드베이스가 커질수록 잠재적 버그에 더 많이 노출된다.

테스트 중에 실행되는 코드의 양
코드 복잡도
코드의 도메인 유의성

리팩터링 내성

bad case ) 
새로운 기능 개발 완료 -> 리팩토링을 조금하고 싶은 마음이 생김 -> 리팩토링을 함 -> 
프로그램의 결과는 잘 나옴, 하지만 실패하는 테스트 케이스 발생 -> 알아보니 아무것도 고장 아님 

이런 상황을 거짓 양성(false positive: 문제가 없는데 문제가 있다는 알림)이라고 한다. 왜 리팩터링 내성을 신경써야 하는가? 단위 테스트의 목표는 프로젝트 성장을 지속 가능하게 하는것이다.

테스트가 지속 가능한 성장을 하게 하는 메커니즘은 회귀 없이 주기적으로 리팩터링하고 새로운 기능을 추가하는 것이다.

이로 얻을 수 있는 장점은 아래 두가지가 있다.

기존 기능이 고장 났을 때 테스트 조기 경고 제공
코드 변경이 회귀로 이어지지 않을 것이라는 확신하게 된다.

하지만 거짓 양성은 두 가지 이점을 모두 방해한다.

테스트가 타당한 이유 없이 실패하면, 코드 문제에 대응하는 능력과 의지가 희석된다. 시간이 흐르면 실패에 익숙해지고 무시하게 된다. 최악의 경우 운영 환경에 배포되기도 한다.

거짓 양성이 빈번하면 테스트 스위트에 대한 신뢰가 서서히 떨어진다. 더 이상 테스트를 믿을 만한 안정망으로 인식하지 않는다.

무엇이 거짓 양성의 원인인가?

거짓 양성의 수는 테스트 구성 방식과 직접적인 관련이 있다. 테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합할수록 허위 경보가 더 많이 생긴다.

따라서 거짓 양성의 가능성을 줄이는 방법은 해당 구현 세부 사항에서 테스트를 분리하는 것이다.

따라서 리팩토링을하게되면 테스트는 매번 실패하게 된다. 이처럼 SUT의 세부 사항과 결합된 테스트는 리팩터링 내성이 없다.

이런 테스트는 회귀가 발생한 적절한 타이밍에 조기 경고를 제공하지 않고 대부분 잘못된 경고를 준다. 리팩터링에 대한 능력과 의지에 방해가 된다.

알고리즘을 검증하는 것이 목표이고 세부 구현이 확인이 목표일 때, 가장 심각하게 깨지기 쉽다.

결합도를 낮추자

코드의 내부 작업과 테스트 사이에 가능한 멀리 떨어뜨리고 최종 결과를 목표로 테스트 코드를 작성하자.

사용자에게 의미 있는 유일한 결과만 테스트 했다! 이런 테스트에는 거짓 양성은 거의 없다.

첫 번째, 두 번째 특성 간의 본질적인 관계

첫 번째 특성(회귀 방지)와 두 번째 특성(리팩터링 내성)는 둘 다 정반대의 관점에서도 테스트 스위트의 정확도에 기여한다.

테스트 정확도 극대화

참 음성 : 테스트 성공 예측 -> 기능 성공
참 양성 : 테스트 실패 예측 -> 기능 실패
거짓 음성 : 테스트 성공 예측 -> 기능 실패
거짓 양성 : 테스트 실패 예측 -> 기능 성공
회귀 방지는 거짓 음성에서 보호하고, 리팩터링 내성은 거짓 양성의 수를 최소화한다.

거짓 음성은 회귀 방지로 피할 수 있고, 거짓 양성은 리팩토리 내성으로 최소화 할 수 있다.

테스트 정확도 = 신호(발견된 버그 수) / 소음(허위 경보 발생 수) = 양성 / 음성

테스트가 버그가 있음을 얼마나 잘 나타내는가? (거짓 음성(회귀 방지)) 테스트가 버그가 없음을 얼마나 잘 나타내는가? (거짓 양성(리팩토링 내성))

거짓 양성과 거짓 음성의 중요성 : 역학 관계

거짓 양성은 단기적으로 거짓 음성만큼 나쁘지는 않다. 즉, 프로젝트 초반에는 크게 신경쓰지 않아도 될 수 있다. 하지만 시간이 지나면서 점차 중요해진다.

프로젝트 초기에는 코드 정리를 많이 할 필요가 없고 새로 작성된 코드는 완벽하고 반짝반짝하다. 그리고 개발자의 기억 속에 아직 생생하기 때문에 테스트에서 잘못된 경보가 발생하더라도 쉽게 리팩터링을 할 수 있다.

하지만 시간이 흐를수록 코드베이스는 나빠진다. 점점 복잡해지고 체계적이지 않게 된다. 이런 경향을 줄이러면 정기적으로 리팩터링을 해야한다. 그렇지 않으면 새로운 기능에 드는 비용이 결국 엄청나게 커진다.

세 번째, 네 번째 특성 : 빠른 피드백과 유지 보수성

테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 수행할 수 있고 더 자주 실행할 수 있다.

테스트가 빠르게 실행되면 코드 결함이 생기자마자 버그에 대해 경고하기 시작할 정도로 피드백 루프를 줄여서 비용을 0까지 줄일 수 있다.

오래 걸리는 테스트는 자주 실해아지 못하기 때문에 잘못된 방향으로 가면서 시간을 더 많이 낭비하게 된다.

유지보수성는 유지비로 평가하고 유지비는 아래 요소로 구성된다.

테스트가 얼마나 이해하기 어려운가 : 테스트의 크기와 관련있다. 테스트가 크면 이해하기 어려워진다. 품질은 유지하되 작게 작성하게

테스트가 얼마나 실행하기 어려운가 : 테스트가 프로세스 외부 종속성(ex. DB)으로 작동한다면, 의존성을 상시 운영하는데 시간을 들여야 한다.

이상적인 테스트를 찾아서

좋은 단위 테스트의 4대 특성

회귀 방지
리팩터링 내성
빠른 피드백
유지 보수성

네 가지 특성을 곱하면 테스트의 가치가 결정된다.

테스트 가치 추정치 = [0 ~ 1] * [0 ~ 1] * [0 ~ 1] * [0 ~ 1]

이상적인 테스트를 만들 수 있는가?

각 특성에서 1에 가까운 점수를 받아야 이상적인 테스트가 된다. 하지만 안타깝게도 이상적인 테스트를 만드는 것은 불가능하다.

3가지 특성 회귀 방지, 리팩터링 내성, 빠른 피드백은 상호 배타적이기 때문이다.

극단적인 사례 1: 엔드 투 엔드 테스트

모든 부분을 테스트하고 실사용 유저와 비슷한 환경에서 테스트한다 : 회귀 방지 우수

거짓 양성에 면역이 돼 래팩터링 내성도 우수하다. 특정 구현을 강요하지 않는다 : 리팩터링 내성 우수

테스트 속도 : 매우 느림

극단적인 사례 2: 간단한 테스트

너무 간단해서 고장이 없을 것 같은 작은 코드 조각을 다른 케이스

Name field에 문제가 있을거 같지는 않다.

매우 빠르게 빠른 피드백 제공하다. 거짓 양성이 생길 가능성 자체가 낮기 때문에 리팩터링 내성도 우수하다.

하지만 문제를 예방하는 기능을 잘 하지는 않았다. 따라서 회귀 방지가 없다.

극단적인 사례 3: 깨지기 쉬운 테스트(Brittle tests)

리팩터링을 견디지 못하고, 해당 기능이 고장 났는지 여부와 상관잆어 테스트 실패했다고 알리는 경우

SQL문을 검증하는 테스트 코드

UserRepository 클래스가 올바른 SQL문을 생성하는지 확인한다고 하자. 버그를 잡을 수 있는가? 가능하다.

그러나 리팩터링 내성이 좋은가? 아래 SQL 문은 여러 가지 형태로 변경해도 결과는 모두 같다. 위의 테스트는 SUT의 내부 구현 세부 사항에 결합되어 있는 예이다.

이상적인 테스트를 찾아서 : 결론

세 가지 특성 모두 완벽한 점수를 얻어서 이상적인 테스트를 만드는 것은 불가능 하다.

네 번째 특성인 유지 보수성은 엔드 투 엔드 테스트를 제외하고 다른 세가지 특성과 상관관계까 없다. 엔드 투 엔드 테스트는 관련된 모든 의존성을 설정해야 하므로 일반적으로 크기가 더 크다.

세 가지 특성 모두 양보할 만큼 서로 조금씩 인정하는 것이 최선의 전략이다.

그러나 실제로는 리팩터링 내성을 포기할 수 없다. 즉 리팩터링 내성을 최대로 하고 회귀 방지와 빠른 피드백 사이에서 줄다리기를 해야한다.

리팩터링 내성을 포기할 수 없는 이유는 테스트가 이 특성을 갖는지 여부가 대부분 이진 선택이기 때문이다. 즉, 리팩터링 내성이 있거나 없거나 둘 중 하나이다.

CAP 정리

일관성(consistency) : 모든 읽기가 가장 최근의 쓰기 또는 오류를 수진하는 것을 의미 가용성(availability) : 모든 요청이 응답을 수신하는 것을 의미 분할 내성(partition tolerance) : 네트워크 분할에도 시스템이 계속 작동함을 의미

세 가지 사이에서 절충안을 선택해야 한다.

대규모 분산 시스템의 분할 내성도 타협할 수 없다.

예를 들어, 아마존 웹 사이트와 같은 경우 대규모 애플리케이션을 단일 머신에서 작동할 수 없다.

시스템 일부는 가용성으 높이고자 일관성을 약간 양보하는 것이 좋다.

예를 들어, 제품 제품 카탈로그를 표시할 때 카탈로그 일부는 오래돼도 일반적으로 괜찮다.

반면에 제품 설명을 업데이트할 때는 가용성보다 일관성이 중요하다.

대중적인 테스트 자동화 개념 살펴보기

테스트 피라미드 분해

피라미드 상단은 회귀 방지에 유리한 반면, 하단은 실행 속도를 강조한다. 하지만 어느 계층도 리팩터링 내성을 포기하지 않는다.

블랙박스 테스트와 화이트박스 테스트 간의 선택

블랙박스 테스트 : 시스템의 내부 구조를 몰라도 시스템의 기능을 검사할 수 있는 소프트웨어 테스트 방법. 일반적으로 명세와 요구 사항이 어떻게 동작해야하는지가 아니라 무엇을 해야하는지를 중심으로 구축된다.

화이트박스 테스트 : 애플리케이션의 내부 작업을 검증하는 테스트 방식, 요구 사항이나 명세가 아닌 소스 코드에서 파생된다

두 가지 방법 모두 장단점이 있다.

화이트박스 테스트가 더 철저한편이다. 하지만 테스트가 테스트 대상 코드의 특정 구현과 결합되어 있기 때문에 깨지기 쉽다.

블랙박스 테스트는 이와 정반대이다.

하지만 리팩터링 내성은 포기하면 안된다. 따라서 블랙박스 테스트를 기본으로 선택해야한다. 모든 테스트(단위 테스트, 통합 테스트, 엔드 투 엔드 테스트)가 시스템을 블랙박스로 보게 만들고 문제 영역에 의미 있는 동작을 확인하라.

유일한 예외는 알고리즘 복잡도가 높은 유틸리티 코드를 다루는 경우이다.

Last updated