티스토리 뷰
도서 정보 : http://www.yes24.com/Product/Goods/104084175
단위 테스트 - YES24
소프트웨어 개발에 있어 단위 테스트는 이제 선택이 아니라 필수가 됐다. 단위 테스트에 대한 오해를 바로잡고, 올바른 단위 테스트에 대한 원칙, 테스트를 작성하는 스타일과 효과적인 테스트
www.yes24.com
정리
1. 제품 코드와 최대한 결합도가 낮은 테스트 코드가 좋은 테스트 코드이다.
2. 회귀 방지 만큼이나 리팩터링 내성 또한 장기적인 소프트웨어 개발을 위해선 중요하다.
3. 이상적인 테스트 코드는 없다. 차선으로 리팩터링 내성을 최대로 하고 회귀 방지와 빠른 피드백 사이에서 조절해라.
리마인드) 1장에서 나왔던 좋은 단위 테스트 스위트의 특성
- 개발 주기에 통합돼 있어야 한다.
- 코드베이스의 가장 중요한 부분만 대상으로 한다.
- 최소한의 유지비로 최대 가치를 끌어낸다.
특히 마지막 특성을 달성하려면 두 가지를 할 수 있어야 한다.
- 가치있는 테스트 (그리고 더 나아가, 낮은 가치의 테스트) 식별
- 가치 있는 테스트 작성
4장은 테스트를 분석할 수 있는 식별하는 방법을 알려준다.
4.1 좋은 단위 테스트의 4대 요소 자세히 살펴보기
좋은 단위 테스트의 4가지 특성
- 회귀 방지
- 리펙터링 내성
- 빠른 피드백
- 유지 보수성
회귀 방지
회귀 : 소프트웨어 버그
자신이 작성한 코드는 자산이 아니라 책임이다. 따라서 코드베이스가 커질수록 잠재적 버그에 더 많이 노출된다.
- 테스트 중에 실행되는 코드의 양
- 코드 복잡도
- 코드의 도메인 유의성
리팩터링 내성
리팩터링 내성이 안 좋은 예시 상황)
새로운 기능 개발 완료 -> 리팩토링을 조금하고 싶은 마음이 생김 -> 리팩토링을 함
-> 프로그램의 결과는 잘 나옴, 하지만 실패하는 테스트 케이스 발생 -> 알아보니 아무것도 고장 아님
이런 상황을 거짓 양성(false positive: 문제가 없는데 문제가 있다는 알림)이라고 한다.
왜 리팩터링 내성을 신경써야 하는가? 단위 테스트의 목표는 프로젝트 성장을 지속 가능하게 하는것이다.
테스트가 지속 가능한 성장을 하게 하는 메커니즘은 회귀 없이 주기적으로 리팩터링하고 새로운 기능을 추가하는 것이다.
이로 얻을 수 있는 장점은 아래 두가지가 있다.
- 기존 기능이 고장 났을 때 테스트 조기 경고 제공
- 코드 변경이 회귀로 이어지지 않을 것이라는 확신하게 된다.
하지만 거짓 양성은 두 가지 이점을 모두 방해한다.
- 테스트가 타당한 이유 없이 실패하면, 코드 문제에 대응하는 능력과 의지가 희석된다. 시간이 흐르면 실패에 익숙해지고 무시하게 된다. 최악의 경우 운영 환경에 배포되기도 한다.
- 거짓 양성이 빈번하면 테스트 스위트에 대한 신뢰가 서서히 떨어진다. 더 이상 테스트를 믿을 만한 안정망으로 인식하지 않는다.
(마치 양치기 소년 이야기와 비슷하다. 빈번한 양치기의 거짓말이 마을 사람들이 늑대 출몰 상황에 대한 대응 의지를 꺽고 양치기에 대한 신뢰도 떨어뜨리는 결과를 낳았다.)
무엇이 거짓 양성의 원인인가?
거짓 양성의 수는 테스트 구성 방식과 직접적인 관련이 있다.
테스트와 테스트 대상 시스템(SUT)의 구현 세부 사항이 많이 결합할수록 허위 경보가 더 많이 생긴다.
따라서 거짓 양성의 가능성을 줄이는 방법은 해당 구현 세부 사항에서 테스트를 분리하는 것이다.
예제)
HTML의 머리글, 본문, 바닥글을 생성하는 MessageRenderer 클래스가 있다고 해보자.
테스트 코드를 보면 하위 렌더링 클래스(HeaderRenderer, Body~, Footer~) 예상하는 모든 유형이고 올바른 순서로 나타나 있다.
보기 좋은 테스트로 보인다.
하지만 MessageRenderer의 식별할 수 있는 동작(인풋, 아웃풋)을 검증하고 있지 않다.
하위 레더링 클래스를 재배열하거나 그 중 하나를 새 것으로 교체하면 어떻게 되는가? 반드시 버그가 생기는 것은 아니지만 대부분의 경우에 그렇다.
만약 BodyRenderer를 완벽히 동일한 작업을 수행하는 BoldRenderer로 교체하면 어떻게 되는가..? 만약 하위 렌더링 클래스를 모두 제거하고 MessageRenderer에서 직접 렌더링 하도록 변경하면 어떻게 되는가..?
이 테스트 코드는 적용할 수 있는 다른 구현을 고려하지 않고 특정 구현만 예상해서 알고리즘을 검사하고 있는 것이다.
따라서 리팩토링을하게되면 테스트는 매번 실패하게 된다. 이처럼 SUT의 세부 사항과 결합된 테스트는 리팩터링 내성이 없다.
이런 테스트는 회귀가 발생한 적절한 타이밍에 조기 경고를 제공하지 않고 대부분 잘못된 경고를 준다. (거짓 양성의 단점 첫번째)
리팩터링에 대한 능력과 의지에 방해가 된다. (버그를 찾을 때 테스트가 아무런 도움이 되지 않는다. 믿지 못하기 때문에. 거짓 양성의 단점 두번째)
알고리즘을 검증하는 것이 목표이고 세부 구현이 확인이 목표일 때, 가장 심각하게 깨지기 쉬운 테스트.
구현 세부 사항 대신 최종 결과를 목표로 하기
깨지기 쉬운 테스트 == 리팩터링 내성이 없는 테스트 코드
깨지기 쉬운 이유 : SUT의 구현 세부 사항과 테스트 간의 결합도가 높아서
결합도를 낮추자!
코드의 내부 작업과 테스트 사이에 가능한 멀리 떨어뜨리고 최종 결과를 목표로 테스트 코드를 작성하자.
사용자에게 의미 있는 유일한 결과만 테스트 했다! 이런 테스트에는 거짓 양성은 거의 없다.
(거의 없는 이유는 메서드에 새 매개변수를 추가하는 등의 컴파일 오류가 발생할 수 있기 때문이다. 기술적으로 이것도 거짓 양성으로 간주하지만 애플리케이션 동작 변경(알고리즘 수정)으로 인한 테스트 실패하는 것은 아니다.)
4.2 첫 번째 특성과 두 번째 특성 간의 본질적인 관계
첫 번째 특성(회귀 방지)와 두 번째 특성(리팩터링 내성)는 둘 다 정반대의 관점에서도 테스트 스위트의 정확도에 기여한다. (회귀 방지는 거짓 음성을 줄여주는 리팩터링 내성은 거짓 양성을 줄이는 방향으로)
테스트 정확도 극대화
참 음성 : 테스트 성공 예측 -> 기능 성공
참 양성 : 테스트 실패 예측 -> 기능 실패
거짓 음성 : 테스트 성공 예측 -> 기능 실패
거짓 양성 : 테스트 실패 예측 -> 기능 성공
거짓 음성은 회귀 방지로 피할 수 있고, 거짓 양성은 리팩토리 내성으로 최소화 할 수 있다.
테스트가 버그가 있음을 얼마나 잘 나타내는가? (거짓 음성(회귀 방지))
테스트가 버그가 없음을 얼마나 잘 나타내는가? (거짓 양성(리팩토링 내성))
테스트 정확도 = 신호(발견된 버그 수) / 소음(허위 경보 발생 수) = 양성 / 음성
거짓 양성과 거짓 음성의 중요성 : 역학 관계
거짓 양성은 단기적으로 거짓 음성만큼 나쁘지는 않다.
즉, 프로젝트 초반에는 크게 신경쓰지 않아도 될 수 있다. 하지만 시간이 지나면서 점차 중요해진다.
프로젝트 초기에는 코드 정리를 많이 할 필요가 없고 새로 작성된 코드는 완벽하고 반짝반짝하다. 그리고 개발자의 기억 속에 아직 생생하기 때문에 테스트에서 잘못된 경보가 발생하더라도 쉽게 리팩터링을 할 수 있다.
하지만 시간이 흐를수록 코드베이스는 나빠진다. 점점 복잡해지고 체계적이지 않게 된다. 이런 경향을 줄이러면 정기적으로 리팩터링을 해야한다. 그렇지 않으면 새로운 기능에 드는 비용이 결국 엄청나게 커진다.
대부분 회귀 방지에만 중점을 두는 경향이 있는데, 회귀 방지는 프로젝트 성장을 유지하는데 도움이 되고, 가치가 있으며
세 번째 요소와 네 번째 요소 : 빠른 피드백과 유지 보수성
테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 수행할 수 있고 더 자주 실행할 수 있다.
테스트가 빠르게 실행되면 코드 결함이 생기자마자 버그에 대해 경고하기 시작할 정도로 피드백 루프를 줄여서 비용을 0까지 줄일 수 있다.
오래 걸리는 테스트는 자주 실해아지 못하기 때문에 잘못된 방향으로 가면서 시간을 더 많이 낭비하게 된다.
유지보수성는 유지비로 평가하고 유지비는 아래 요소로 구성된다.
- 테스트가 얼마나 이해하기 어려운가 : 테스트의 크기와 관련있다. 테스트가 크면 이해하기 어려워진다. 품질은 유지하되 작게 작성하게
- 테스트가 얼마나 실행하기 어려운가 : 테스트가 프로세스 외부 종속성(ex. DB)으로 작동한다면, 의존성을 상시 운영하는데 시간을 들여야 한다.
4.4 이상적인 테스트를 찾아서
좋은 단위 테스트의 4대 특성
- 회귀 방지
- 리팩터링 내성
- 빠른 피드백
- 유지 보수성
네 가지 특성을 곱하면 테스트의 가치가 결정된다.
테스트 가치 추정치 = [0 ~ 1] * [0 ~ 1] * [0 ~ 1] * [0 ~ 1]
이상적이 테스트를 만들 수 있는가?
각 특성에서 1에 가까운 점수를 받아야 이상적인 테스트가 된다. 하지만 안타깝게도 이상적인 테스트를 만드는 것은 불가능하다.
3가지 특성 회귀 방지, 리팩터링 내성, 빠른 피드백은 상호 배타적이기 때문이다.
극단적인 사례 1: 엔드 투 엔드 테스트
모든 부분을 테스트하고 실사용 유저와 비슷한 환경에서 테스트한다 : 회귀 방지 우수
거짓 양성에 면역이 돼 래팩터링 내성도 우수하다. 특정 구현을 강요하지 않는다 : 리팩터링 내성 우수
테스트 속도 : 매우 느림
극단적인 사례 2: 간단한 테스트
너무 간단해서 고장이 없을 것 같은 작은 코드 조각을 다른 케이스
매우 빠르게 빠른 피드백 제공하다.
거짓 양성이 생길 가능성 자체가 낮기 때문에 리팩터링 내성도 우수하다.
하지만 문제를 예방하는 기능을 잘 하지는 않았다. 따라서 회귀 방지가 없다.
극단적인 사례 3: 깨지기 쉬운 테스트(Brittle tests)
리팩터링을 견디지 못하고, 해당 기능이 고장 났는지 여부와 상관잆어 테스트 실패했다고 알리는 경우
UserRepository 클래스가 올바른 SQL문을 생성하는지 확인한다고 하자. 버그를 잡을 수 있는가? 가능하다.
그러나 리팩터링 내성이 좋은가?
아래 SQL 문은 여러 가지 형태로 변경해도 결과는 모두 같다.
위의 테스트는 SUT의 내부 구현 세부 사항에 결합되어 있는 예이다.
이상적인 테스트를 찾아서 : 결론
세 가지 특성 모두 완벽한 점수를 얻어서 이상적인 테스트를 만드는 것은 불가능 하다.
네 번째 특성인 유지 보수성은 엔드 투 엔드 테스트를 제외하고 다른 세가지 특성과 상관관계까 없다. 엔드 투 엔드 테스트는 관련된 모든 의존성을 설정해야 하므로 일반적으로 크기가 더 크다.
세 가지 특성 모두 양보할 만큼 서로 조금씩 인정하는 것이 최선의 전략이다.
그러나 실제로는 리팩터링 내성을 포기할 수 없다. 즉 리팩터링 내성을 최대로 하고 회귀 방지와 빠른 피드백 사이에서 줄다리기를 해야한다.
리팩터링 내성을 포기할 수 없는 이유는 테스트가 이 특성을 갖는지 여부가 대부분 이진 선택이기 때문이다. 즉, 리팩터링 내성이 있거나 없거나 둘 중 하나이다.
CAP 정리
일관성(consistency) : 모든 읽기가 가장 최근의 쓰기 또는 오류를 수진하는 것을 의미
가용성(availability) : 모든 요청이 응답을 수신하는 것을 의미
분할 내성(partition tolerance) : 네트워크 분할에도 시스템이 계속 작동함을 의미
세 가지 사이에서 절충안을 선택해야 한다.
대규모 분산 시스템의 분할 내성도 타협할 수 없다.
예를 들어, 아마존 웹 사이트와 같은 경우 대규모 애플리케이션을 단일 머신에서 작동할 수 없다.
시스템 일부는 가용성으 높이고자 일관성을 약간 양보하는 것이 좋다.
예를 들어, 제품 제품 카탈로그를 표시할 때 카탈로그 일부는 오래돼도 일반적으로 괜찮다.
반면에 제품 설명을 업데이트할 때는 가용성보다 일관성이 중요하다.
4.5 대중적인 테스트 자동화 개념 살펴보기
테스트 피라미드 분해
피라미드 상단은 회귀 방지에 유리한 반면, 하단은 실행 속도를 강조한다.
하지만 어느 계층도 리팩터링 내성을 포기하지 않는다.
블랙박스 테스트와 화이트박스 테스트 간의 선택
- 블랙박스 테스트 : 시스템의 내부 구조를 몰라도 시스템의 기능을 검사할 수 있는 소프트웨어 테스트 방법. 일반적으로 명세와 요구 사항이 어떻게 동작해야하는지가 아니라 무엇을 해야하는지를 중심으로 구축된다.
- 화이트박스 테스트 : 애플리케이션의 내부 작업을 검증하는 테스트 방식, 요구 사항이나 명세가 아닌 소스 코드에서 파생된다
두 가지 방법 모두 장단점이 있다.
화이트박스 테스트가 더 철저한편이다. 하지만 테스트가 테스트 대상 코드의 특정 구현과 결합되어 있기 때문에 깨지기 쉽다.
블랙박스 테스트는 이와 정반대이다.
하지만 리팩터링 내성은 포기하면 안된다. 따라서 블랙박스 테스트를 기본으로 선택해야한다. 모든 테스트(단위 테스트, 통합 테스트, 엔드 투 엔드 테스트)가 시스템을 블랙박스로 보게 만들고 문제 영역에 의미 있는 동작을 확인하라.
유일한 예외는 알고리즘 복잡도가 높은 유틸리티 코드를 다루는 경우이다.
'개발 > Unit testing' 카테고리의 다른 글
Unit Testing - 5장 목과 테스트 취약성 (0) | 2022.04.09 |
---|---|
Unit Testing - 3장 단위 테스트의 구조 (0) | 2022.03.26 |
Unit Testing - 2장. 단위 테스트란 무엇인가 (0) | 2022.02.25 |
Unit Testing - 1장. 단위 테스트의 목표 (0) | 2022.02.18 |