OTTER-LOG

4장, 좋은 단위테스트

단위 테스트, 블라디미르 코리코프 저/임준혁 역by otter2023년 1월 4일에 최종수정되었습니다.
잘못된 내용이 있으면 댓글을 달아주세요.

회귀 방지

회귀는 코드를 수정한 후 (일반적으로 새로운 기능을 출시한 후) 기능이 원할하게 동작하지 않는 경우를 말한다. 좋은 단위테스트는 이러한 회귀를 방지할 수 있어야 한다. 회귀 방지 지표에 대해 평가하려면 다음 사항을 고려할 수 있다.

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

일반적으로 실행되는 코드가 많을수록 테스트에서 회귀가 나타날 가능성이 높다. 그 중에서도, 복잡한 비즈니스 로직을 나타내는 코드가 더욱 중요하다. (코드 도도메인의 유의성 부분) 비즈니스에 중요한 기능에서 발생한 버그에 가장 큰 피해가 있기 때문이다.

이런 점에 따라, 단순한 코드를 테스트 하는 것은 가치가 없다. 짧고, 비즈니스 로직을 많이 담고 있지도 않은 코드는 코드를 다루는 데에 실수할 여지가 많지 않고 회귀 오류를 많이 발생시키지 않기 때문이다.

결과적으로, 많은 코드의 양을 테스트 하는 것이 회귀방지에 좋지만 이를 할 때에 해당 코드가 테스트를 할만한 가치가 없는지 판단하는 것 또한 중요하다. 물론, 핵심 비즈니스 코드를 담고있는 부분은 꼭 테스트 해야한다.


리팩터링 내성

리팩터링은 동작을 수정하지 않고 기존 코드를 변경하는 것을 의미한다.

이 의도는, 코드의 비기능적 특성 - 메서드 이름을 바꾸거나, 코드 조각을 클래스로 추출하는 - 을 개선하는 것으로 가독성을 높이고 복잡도를 낮추는 데에 목적을 둔다

거짓 양성

리팩터링을 할 때에, 코드의 기능은 원할히 동작하지만 잘못된 테스트로 인해 테스트에 오류가 발생할 수 있다. 이러한 부분을 거짓양성이라고 한다. 구현을 수정하지만 식별할 수 있는 동작은 유지할 때에 발생한다. 리팩터링 내성은 이 거짓 양성과 관련이 있다.

리팩터링 내성 지표에서 테스트 점수가 얼마나 잘 나오는지 평가하기 위해 얼마나 많은 거짓 양성일 발생하는지 살펴봐야 한다. 물론 적을수록 좋다.

거짓양성이 중요한 이유는, 전체 테스트 스위트에 치명적인 영향을 줄 수 있기 때문이다. 단위테스트의 목표가 프로젝트의 성장의 지속가능성이라고 한다면, 리팩터링 내성이 적은 코드는 리팩터링할때마다 테스트에 오류가 발생하고 테스트코드 또한 수정해야 한다. 이러한 거짓된 오류는, 테스트에 대한 신뢰 감소로 이어질 수 있고 때로는 리팩터링 자체를 회피하게 될 수 있다.

구현 세부사항 대신 최종 결과를 목표로 하기

구현 세부사항과, 테스트 간의 결합도를 낮추는 방법을 사용해야 한다. 클래스의 모든 세부사항을 테스트하는 것이 아닌, 테스트가 최종적으로 결과로 만들어낼 결과를 테스트한다.


테스트 정확도

| | 작동 | 고장 | | ------ | --------------------- | ------------------- | | 테스트 통과 | 올바른 추론 | 2종 오류 (거짓 음성) 회귀방지 | | 테스트 실패 | 1종 오류 (거짓 양성) 리팩터링 내성 | 올바른 추론 |

기능이 작동할때 통과하거나 기등이 고장났을때 실패하는 것은 올바른 추론이 된다. 다만, 기능은 고장났지만 테스트가 통과하는 경우 거짓음성이 되고 이는 회귀방와 관련이 있다. 반면, 기능은 작동하지만 테스트가 실패하는 경우 거짓양성이 되고 리팩터링 내성과 관련이 있다.

정확도의 지표는 이 거짓음성과 양성을 제외하고 통과와 실패를 얼마나 잘 나타내는지와 관련이 있다. 그러므로, 거짓음성과 거짓양성을 모두 신경쓰며 테스트코드를 작성하는 것이 중요하다.

(이 부분에서 초반에는 회귀방지가 더 중요하고, 후반에는 리팩터링 내성이 점차 중요해지는 특징이 있지만 중대형프로젝트라면 두 부분 모두 신경쓰며 작성해야 한다. )


빠른 피드백과 유지 보수성

이 부분에서는 다음 두가지 특성을 고민해야 한다.

  • 테스트가 얼마나 이해하기 어려운가?

    테스트는 코드라인이 적을 수록 읽기 쉽고 바꾸기도 쉽다. 다만 이를 위해 테스트 코드를 인위적으로 줄이는 것은 좋지 못하다. 테스트 코드의 품질은 제품 코드만큼 중요하다.

  • 테스트가 얼마나 실행하기 어려운가?

    테스트가 프로세스 외부종속성으로 작동하면, 의존성을 상시 운영하는 데에 시간이 든다.


이상적인 테스트를 찾아서

좋은 단위테스트는 위의 4가지 요소를 모두 포함하고 있는 테스트를 말한다.

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

이 네가지 특성을 곱하면 테스트의 가치가 결정된다. (결국 중요하지 않은 부분은 없다. ) 물론 테스트의 가치를 정확히 측정하는 것은 어렵다. 그렇지만, 이러한 평가요소를 통해 추정치를 측정해 판단할 수 있다.

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

엔드투엔드 테스트는 최종 사용자 입장에서 시스템을 살펴본다.UI, 데이터페이스, 외부어플리케이션 을 모두 포함해 진행된다. 이러한 테스트는 많은 코드를 테스트하므로 회귀방지를 훌륭히 해내고, 리팩터링 내성도 우수하다. (모든 기능을 테스트하고, 식별할 수 있는 동작만을 테스트하기 때문이다.)

그러나 이러한 엔드투엔드 테스트는 느리다. 모든 시스템에 대한 테스트는 빠른 피드백을 받기 어려워진다.

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

너무나도 간단해, 고장이 없을 것 같은 작은 코드를 테스트하는 것은 매우 빠르게 실행되고 빠른 피드백을 제공받을 수 있지만 검증이 무의미하다. 따라서, 회귀방지에 적절한 역할을 하지 못한다.

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

깨지기 쉬운 테스트는, 리팩터링 내성을 견디지 못하고 테스트에 계속해 실패하게 된다. 특히 이런 부분은 결과에 집중하지 않고 구현 세부사항을 테스트할 때에 많이 일어난다.

결과적으로 이러한 극단적 사례를 통해 볼 때에, 이상적인 테스트 - 모든 네가지 요소가 100점을 맞는 - 경우는 거의 없다. 각 요소가 상호 배타적인 특징을 가지고 있기 때문이다. 어느정도 절충안을 찾아야 하지만 그렇다고 어떠한 한 부분을 포기할 수 없다.


블랙박스, 화이트박스

블랙박스 테스트는 시스템의 내부 구조를 몰라도, 시스템의 기능을 검사한다. 어플리 케이션이 어떻게 해야하는지를 검증하지 않고 무엇을 해야 하는지를 중심으로 구축된다.

화이트박스 테스트는 이와 정 반대로, 어플리케이션의 내부 작업을 검증한다. 테스트는 소스코드에서 파생된다.

일반적으로 화이트 박스 테스트가 - 소스코드 분석을 통해 외부 명세에 의존할 때 많은 오류를 발견할 수 있으므로 - 철저한 편이지만 화이트 박스 테스트는 테스트 대상의 특정 코드와 깊게 결합되어 있기 때문에 깨지기 쉽다. 블랙박스 테스트는 이와 정반대의 장단점을 제공한다.

이를 통해 볼 때에 테스트코드를 작성할 때에는 블랙박스 테스트 방식으로 하는 것이 좋다. - 다른 소스 코드를 전혀 모르는 것처럼 - 테스트 하는 방식을 통해, 의미있는 동작을 확인할 수 있기 때문이다.

다만, 테스트를 분석할 때에는 화이트박스 테스트를 하는 것이 좋다.

  • 화이트박스 테스트

(ref : https://github.com/goldbergyoni/javascript-testing-best-practices/blob/master/readme.kr.md#섹션-0️⃣-황금률 )

class ProductService{ // 이 method 는 내부에서만 사용됩니다. // 이 이름을 변경하면 테스트가 실패합니다. calculateVAT(priceWithoutVAT){ return {finalPrice: priceWithoutVAT * 1.2}; // 결과 형식이나 키 이름을 변경하면 테스트가 실패합니다. } // public method getPrice(productId){ const desiredProduct= DB.getProduct(productId); finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; } } it("화이트박스 테스트: 내부 method가 VAT 0을 받으면 0을 반환합니다.", async () => { // 사용자가 VAT를 계산할 수 있게 하는 요구사항은 없으며, 최종 가격만 표시합니다. // 그럼에도 불구하고 여기에서 내부 테스트 수행 expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); });

위의 화이트박스테스는 내부의 구성요소 - finalPrice 라는 이름이 바뀌게 되면 깨지게 된다. 내부에서만 쓰는 모든 테스트 - 특히 내부의 코드와 깊게 결합되어 있는 - 테스트들은 테스트의 유지보수를 어렵게 만든다. 만약 위의 상태에서 calculateVATreturn 하는 객체의 키 값이 바뀌게 되면 테스트 또한 수정해주어야 하기 때문이다.