본문 바로가기

기타

좋은 단위 테스트의 속성(FIRST)

이전 포스트에서 언급한 JUnit 단위 테스트 도서에서 좋은 단위 테스트가 가져야 할 요건에 대하여 설명하고 있기에 옮겨적어본다.

 

만약 단위 테스트가 다음 중 하나의 경우라도 해당된다면 이는 뭔가 문제점이 있고 개선이 필요하다는 것을 의미한다.

  1. 테스트를 사용하는 사람에게 어떤 정보도 주지 못하는 테스트
  2. 산발적으로 실패하는 테스트
  3. 어떤 가치도 증명하지 못하는 테스트
  4. 실행하는 데 오래 걸리는 테스트
  5. 코드를 충분히 커버하지 못하는 테스트
  6. 구현과 강하게 결합되어 있어 작은 변화에도 다수의 테스트가 깨지는 테스트
  7. 수많은 설정 고리로 점프하는 난해한 테스트

이를 해결하기 위해 단위 테스트가 가져야 할 다섯가지 원칙을 제시하고 있으며 이는 Fast, Isolated, Repeatable, Self-validating, Timely의 두문자를 따서 "FIRST"라는 조건으로 불린다.

First, 빠르다

https://cloudlibrariandownunder.wordpress.com/2016/09/05/speedy-coding-and-discovery-how-fast-is-fast-worldcat-libraries-australia-and-british-library-bnb-numbers/

단위 테스트의 빠르고 느림은 다소 주관적인 부분이 있지만 대개 단위 테스트는 내부 코드만 테스트할 시, 즉 외부 데이터베이스나 파일 입출력을 수행하지 않는 경우 수 밀리초를 소모하지만 외부 자원을 다룰 경우 시스템에 따라 더 많은 시간이 소모될 수 있다. 시스템을 설계하여 단위 테스트를 수행할 때 수천개의 단위 테스트를 수행하게 된다면 이에 소모되는 시간은 기하급수적으로 늘어날 것이며 한 테스트가 평균 200밀리초를 소모한다면 2500개 정도의 테스트는 8분 이상 시간을 소모하게 된다.

 

이렇게 단위 테스트에 필요한 시간이 점점 늘어나서 하루에 여러 번 수행하기가 어렵다면 이는 단위 테스트 스위트에 무언가 문제가 있다는 것을 의미한다. 단위 테스트는 빠르게 수행되어 대상 시스템에 대한 지속적이고 종합적인 빠른 피드백을 주는 데 그 가치가 있기 때문에 이를 달성하지 못한다면 테스트를 작성하는 것 자체가 무의미해질 수 있다.

 

그렇기 때문에 최대한 느린 것에 의존하는 테스트 코드를 지양해야 할 것이다.

Isolated, 고립시킨다

https://www.glutenfreetherapeutics.com/living-gluten-free/gluten-free-living/celiac-isolation-inevitable/

좋은 단위 테스트는 검증하려는 작은 양의 코드(Unit)에 집중해야 한다. 하나의 테스트 코드가 다른 테스트 코드와 상호작용하거나 외부 소스에 의존하는 프로덕션 코드에 대하여 테스트를 수행할 경우 테스트를 깨뜨리는, 통제할 수 없는 외부 상황에 의해 테스트가 실패할 수도 있으며 외부 저장소에 대한 가용성, 접근성 문제 역시 발생할 수 있다.

 

예를 들어 어떤 높은 비용의 자원에 대하여 여러 테스트 케이스가 같이 순차적으로 돌아가면서 사용할 경우 테스트가 실패했을 때 앞선 테스트를 추적하여 원인을 찾아내느라 긴 시간을 소모하게 될 수도 있다.

 

그렇기 때문에 좋은 단위 테스트는 다른 단위 테스트나 같은 메소드에 있는 다른 테스트 케이스와도 의존하지 않는 고립된 상태여야 한다. 이는 어떤 순서나 시간에도 관계없이 실행될 수 있어야 하며 테스트 실패 시 분명한 원인을 제공할 수 있도록 집중적인 단위로 나뉘어야 한다.

Repeatable, 반복 가능해야 한다

https://ko.wikipedia.org/wiki/%ED%8C%8C%EC%9D%BC:Repeat_font_awesome.svg

완성된 하나의 단위 테스트는 결과가 어떻게 나올지 명확해야 하며 모든 결과는 통제 아래 있어야 한다. 반복적으로 사용할 수 있는 테스트는 실행할 때마다 결과가 같아야 한다. 이를 위해서 가장 좋은 방법은 직접 통제할 수 없는 외부 환경에 있는 항목들과 격리시키는 것이나 항상 격리시킬 수는 없고 언젠가 시스템은 불가피하게 외부 요소와 상호작용하게 될 것이다.

 

대표적으로 '시간'을 테스트하는 경우를 예로 들어보자. 어떤 값이 자료구조에 추가되었을 때 이에 대한 타임스탬프를 남기는 기능을 테스트하려고 한다면 시간은 항상 흐르고 있으므로 추가되었을 때 남겨진 타임스탬프와 실제로 테스트할 때 비교하는 타임스탬프의 간격이 발생할 수 있으므로 이를 단언하기란 쉽지 않을 것이다. 어떨 때는 오차범위 이내로 비교되어 통과하거나, 그렇지 않을 수도 있다. 이는 산발적인 테스트 성공/실패로 해당 테스트 실패가 거짓인지 진실인지 알 수 없어 1종/2종 오류를 범하게 될 수 있으므로 좋은 단위 테스트라 할 수 없다.

 

그렇기 때문에 이때는 테스트 대상 코드의 나머지를 격리하고 독립성을 유지하기 위해 목 객체(Mock Object)를 활용하는 방안 등을 사용하여야 한다. 시간 관련 라이브러리를 사용하여 가짜 시간 객체를 넘겨서 출처에 상관없이 비교하는 것이다. 이외의 경우(데이터베이스 등)에는 아예 다른 개발자들과의 충돌을 피하기 위해 따로 샌드박스 환경을 만들어 테스트하거나 별도의 서버를 따로 구축할 수도 있을 것이다.

Self-validating, 스스로 검증 가능해야 한다

https://www.sitepoint.com/validating-your-data-with-respect-validation/

JUnit을 사용하지 않고 테스트를 수행한다면 어땠을까? 기존의 나라면 System.out.println()으로 결과를 로그로 출력하고 이를 하나하나 비교해 보면서 기능이 제대로 동작하는지 확인하고 있었을 것이다. 나름 머리를 좀 더 써서 기대하는 값과 출력된 값이 일치하는지 비교할 수 있는 메소드를 만들 수도 있고 주석으로 세부사항이나 다음에 수행할 테스트를 남겨둘 수도 있겠지만 이는 어떻게 보면 "Reinventing the wheel", 즉 이미 만들어진 것(JUnit)을 다시 만들고 있는 행위에 불과하다고 할 수 있다.

 

테스트 결과를 수동으로 비교하는 것은 시간을 소모하고 지치는 일이며 수많은 로그의 바다를 헤엄치다 정작 중요한 신호를 놓칠 수도 있다. 그렇기 때문에 테스트는 해당 작업(Act)의 결과값이 옳은지 스스로 검증 가능하며 이를 준비하는 작업 역시 스스로 수행할 수 있어야 한다. 발전된 테스트 체계의 경우 자가 검증성을 극대화 하여 시스템에 변화가 생긴다면 자동으로 테스트를 수행하도록 하는 경우도 있다. 대표적으로 Infinitest라는 도구가 있으며 아래의 이미지에서 보듯 시스템의 변화에 따라 자동적으로 테스트를 수행한다.

https://infinitest.github.io/

좀 더 큰 규모에서는 Jenkins나 TeamCity같은 지속적 통합(CI, Continuous Integration) 도구를 사용할 수 있다. 이는 소스 저장소를 관찰하여 변화를 감지하면 빌드와 테스트 절차를 자동적으로 수행한다. 여기서 빌드 서버가 이를 판단하여 프로덕션 시스템에 변경 사항을 반영한다면 지속적 배포(CD, Continuous Delivery)를 수행하는 것이며 이는 제품 배포에 소모되는 오버헤드를 크게 줄일 수 있다.

Timely, 적시에 사용한다

https://www.xebi.com/10-tips-to-receive-timely-payments-from-your-clients

사실 프로덕션 시스템에 따라 단위 테스트는 별로 필요없을 수도 있다. 강요되는 사항이 아닌 만큼 언제라도 그만둘 수 있고 언제라도 적용할 수 있다. 어쩔땐 레거시 코드에 단위 테스트를 적용할 수도 있겠지만 가능하면 적절한 순간에 단위 테스트에 집중하는 편이 권장된다. 옛날 코드에 큰 결함이 없고 당장 변경사항을 적용해야 하는 경우가 아니라면 레거시에 대한 단위 테스트보다는 역동적이고 잔고장이 많은 부분에 수행하는 것이 바람직하다.

 

단위 테스트는 수행하면 수행할수록 어느 부분에 테스트가 필요한지 명확해지기 때문에 테스트 대상 코드가 줄어들며 이런 경험이 반복될수록 테스트를 작성하기 쉬워진다. 또한 새로운 코드를 작성한 후에 테스트를 수행하면 그 효과를 즉시 볼 수 있다. 이런 장점들로 인해 소프트웨어 개발 프로세스에 테스트 주도 개발(TDD, Test Driven Development)같은 프로세스가 등장하였다고 할 수 있다.

 

단위 테스트는 소프트웨어 개발의 완성도, 품질을 높이는 좋은 습관이며 이를 이용하여 코드를 검증하는 습관을 가지지 않는다면 코드에 결함이 발생할 확률이 높아진다. 그렇기 때문에 많은 개발팀에서는 팀의 단위 테스트에 대하여 엄격하게 수행하고 있다.

마치며

이렇게 블로그에 포스팅하면서 읽어나가는 것도 꽤 괜찮은 방법인 것 같다. 나중에 머릿속에 더 잘 남을지도 모르긴 하나 읽는데 시간이 좀 걸린다는게 아쉬운 점이다.