개발 공부 기록하기/20. 일반

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

lannstark 2021. 11. 28. 19:48

좋은 단위 테스트에는 다음 4가지 특성이 있다.

  1. 회귀 방지
  2. 리팩토링 내성
  3. 빠른 피드백
  4. 유지 보수성

 

하나씩 살펴보자

회귀 방지

회귀방지란 SW 버그를 방지할 수 있어야 한다는 의미이다. 코드 수정 후 버그가 있었는데 테스트가 통과하면 안된다.

회귀방지를 평가하려면 다음 사항을 고려해야 한다.

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

회귀방지를 극대화하려면 테스트가 가능한 많은 코드를 실행하는 것을 목표로 해야 한다.

 

리팩토링 내성

리팩토링 내성은 테스트를 실패로 바꾸지 않고 기본 애플리케이션 코드를 리팩토링 할 수 있는지에 대한 척도이다. 여기서 말하는 리팩토링이란 코드의 비기능적 특징을 개선하는 것으로 가독성을 높이고 복잡도를 낮추는 것이다. 예를 들어 메소드 이름을 바꾸거나 코드 조각을 새로운 클래스로 추출하는 것을 생각해볼 수 있다

리팩토링으로 아무것도 고장나지 않았고 기능은 예전과 같이 완벽하게 작동하는데 기반 코드를 수정하여 테스트가 빨간색으로 바뀌었다면, 이런 상황을 거짓 양성이라고 한다.

테스트가 SW의 지속 가능한 성장을 하게 하는 매커니즘은 회귀 없이 주기적으로 리팩토링하고 새로운 기능을 추가할 수 있는 것이다. 여기에는 두 가지 장점이 있다.

  • 기존 기능이 고장 났을 때 테스트가 조기 경고를 제공한다. 이러한 조기 경고 덕분에 결함이 있는 코드가 운영 환경에 배포되기 훨씬 전에 문제를 해결할 수 있다. 운영 환경이었으면 문제를 처리하는 데 훨씬 많은 노력이 필요했을 것이다.
  • 코드 변경이 회귀로 이어지지 않을 것이라 확신하게 된다. 이러한 확신이 없으면 리팩토링을 하는데 주저하게 되고 코드베이스가 나빠질 가능성이 훨씬 높아진다.

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

  • 테스트가 타당한 이유 없이 실패하면, 코드 문제에 대응하는 능력과 의지가 희석된다. 시간이 흐르면서 그러한 실패에 익숙해지고 그만큼 신경을 많이 쓰지 않는다. 이내 타당한 실패도 무시하기 시작해 기능이 고장 나도 운영 환경에 들어가게 된다.
  • 반면 거짓 양성이 빈번하면 테스트 스위트에 대한 신뢰가 서서히 떨어지며, 더 이상 믿을만한 안전망으로 인식하지 않는다. 즉, 허위 경보로 인식이 나빠진다. 이렇게 신뢰가 부족해지면 리팩토링이 줄어든다. 회귀를 피하려고 코드 변경을 최소한으로 하기 때문이다.

테스트는 최종 사용자의 관점에서 SUT를 검증해야 하고 최종 사용자에게 의미 있는 결과만 확인하게 해야 한다. 다른 모든 것은 무시해야 한다.

최종 결과가 바뀌지 않을지라도, 테스트를 수행하면 빨간색으로 바뀌는 경우는 테스트가 SUT가 생성한 결과가 아닌 SUT의 구현 세부사항과 결합했기 때문이다.

테스트를 깨지지 않게 하려고 리팩토링 내성을 높이는 방법은 SUT의 구현 세부사항과 테스트 간의 결합도를 낮추는 것 뿐이다. 즉, 코드의 내부 작업과 테스트 사이를 가능한 한 멀리 떨어뜨리고 최종 결과를 목표로 해야 한다.

 

빠른 피드백, 유지 보수성

느린 테스트는 피드백을 느리게 하고 잠재적으로 버그를 뒤늦게 눈에 띄게 해서 버그 수정 비용이 증가한다. 오래 걸리는 테스트는 자주 실행하지 못하기 때문에 잘못된 방향으로 가면서 시간을 더 많이 낭비하게 된다.

유지 보수성은 2가지 주요 요소로 평가할 수 있다.

  1. 테스트가 얼마나 이해하기 어려운가
  2. 테스트가 얼마나 실행하기 어려운가

 

이상적인 테스트

  • end-to-end 테스트는 회귀 오류와 거짓 양성에 대한 보호를 훌륭히 해내지만, 빠른 피드백의 지표에서 실패했다.
  • 간단한 테스트는 우수한 리팩토링 내성과 빠른 피드백을 제공하지만 회귀 방지가 없다.
  • 깨지기 쉬운 테스트는 빠르게 실행되고 회귀 방지를 훌륭히 해내지만, 리팩토링 내성은 거의 없다

좋은 테스트를 만드는 특성 간에 균형을 이뤄내는 것은 쉽지 않다. 세 가지 범주에서 점수를 모두 최대로 낼 수 없고, 유지 보수 관점을 계속 지켜야 테스트가 꽤 짧아지고 간결해진다. 따라서 절충해야 한다. 부분적으로 그리고 전략적으로 희생해야 한다.

 

어떻게 전략적 절충을 해야 할까?

먼저 리팩토링 내성은 포기할 수 없다. 따라서 테스트가 얼마나 버그를 잘 찾아내는지 (회귀방지)와 얼마나 빠른지 (빠른 피드백) 사이의 선택으로 절충이 귀결된다. (CAP 정리와 매우 유사하다)

리팩토링 내성을 포기할 수 없는 이유는 테스트가 이 특성을 갖고 있는지 여부는 대부분 이진 선택이기 때문이다. 즉 테스트에 리팩토링 내성이 있거나 없거나 둘 중 하나다. 그 사이에 중간 단계는 없다. 따라서 리팩토링 내성을 조금만 인정할 수는 없다.

테스트 유형 간의 정확한 비율은 각 팀과 프로젝트마다 다를 것이다. 그러나 일반적으로 피라미드 형태를 유지해야 한다. 즉 end-to-end 테스트가 가장 적고, 단위 테스트가 가장 많으며, 통합 테스트는 중간 어딘가에 있어야 한다.

이러한 테스트 피라미드에는 예외가 있다. 비즈니스 규칙이나 복잡도가 거의 없는 기본적인 CRUD 작업이라면, 테스트 '피라미드'는 단위 테스트와 통합 테스트의 수가 같고 end-to-end 테스트가 없는 직사각형 처럼 보일 것이다.

단위 테스트는 알고리즘이나 비즈니스 복잡도가 없는 상황에서는 유용하지 않으므로 간단한 테스트 수준까지 빠르게 내려간다. 반면 통합 테스트는 그 가치가 잘 지켜진다. 코드가 아무리 단순하더라도 DB와 같이 하위 시스템과 통합돼 잘 작동하는지 확인하는 것이 중요하다. 결국 단위 테스트는 더 적어지고 통합 테스트가 더 많아질 수 있다.

블랙박스 테스트 / 화이트박스 테스트

  • 블랙박스 테스트 : 시스템의 내부 구조를 몰라도 시스템의 기능을 검사할 수 있는 SW 테스트 방법이다. 일반적으로 명세와 요구사항, 즉 애플리케이션이 어떻게 해야 하는지가 아니라 무엇을 해야 하는지를 중심으로 구축된다.
  • 화이트박스 테스트 : 애플리케이션 내부 작업을 검증하는 테스트 방식이며, 테스트는 요구사항이나 명세가 아닌 소스 코드에서 파생된다.

화이트박스 테스트 대신 블랙박스 테스트를 기본으로 선택하라. 모든 테스트가 시스템을 블랙박스로 보게 만들고 문제 영역에 의미 있는 동작을 확인하라. 테스트를 통해 비즈니스 요구 사항으로 거슬러 올라갈 수 없다면, 이는 테스트가 깨지기 쉬움을 나타낸다. 이 테스트를 재구성하거나 삭제하라. 기존 테스트 스위트로 두지 말라. 유일한 예외는 알고리즘 복잡도가 높은 유틸리티 코드를 다루는 경우이다.