좋은 유닛 테스트를 찾아서
좋은 유닛 테스트를 짜고 싶어!
나는 테스트 작성하는 것을 은근히 즐긴다. 파란불이 하나씩 들어오는 쾌감이 좋다. 게다가 좋은 테스트를 작성한 것 같은 착각(?)을 느낄 때는 뿌듯함이 몇 배로 느껴진다.
내가 의미있다고 생각하는 테스트는 비즈니스 로직을 잘 검사하고, 테스트를 읽는 이로 하여금 프로덕션 코드를 잘 설명해주는 테스트이다. 버그 이슈를 처리하면서 해당 버그를 만들어낸 조건을 테스트에 추가할 때도 묘한 희열을 느낀다. 안타깝게도 나는 버그를 만들었지만, 이 테스트가 존재하는 한 똑같은 버그가 생길 일은 없을테니까!
하지만 자괴감이 드는 테스트를 만드는 날도 자주 있다. 이런 테스트는 바로 테스트를 위한 테스트이다. 테스트를 안 만들면 안 될 것 같아서 억지로 만든 거다. 수많은 클래스들을 종합해서 결과를 만들어내는 클래스를 테스트하는 경우에 주로 이런 테스트를 만드는 것 같다. 이런 테스트에서는 mock된 수많은 객체들을 호출하는지 정도만 검사할 뿐, 딱히 로직을 테스트하는 부분이 없다. 테스트를 작성하는 과정이 매우 지루하고, 파란불이 들어오게 만드는 데 많은 짜증이 유발되었다. 결과적으로 너무 길어진 테스트는 작성자인 나도 보기 싫을 정도였다.
때로는 다른 사람이 작성한 테스트를 보고 화가 나기도 했다. 테스트가 생산성을 갉아먹는 부채가 될 수 있다는 사실을 몸소 경험해봤다. 이런 테스트는 처음 작성된 이후 (프로덕션 코드가 수많은 변경을 거치는 동안)리팩토링이 거의 되지 않은 상태였다. 통과가 되는 테스트 케이스는 프로덕션 코드와 완전히 동떨어져 있는 이상한 내용이었다. (아마도) 급한 일정 때문에 미처 내용을 수정하지 못한 테스트 케이스는 ignore되어 있었다. 인내심을 가지고 읽어본 결과 지우고 수정해야 할 코드 투성이었다. 오히려 테스트를 보는 것이 프로덕션 코드의 기능을 이해하는 데 방해가 되었다.
나 스스로도 딱히 도움이 되지 않는 테스트를 작성하기도 하고, 기존에 작성된 테스트에 발목이 붙잡히기도 하면서 자연스레 좋은 테스트가 과연 무엇이고, 어떻게 작성해야 하는 것인지 궁금해졌다. 의미있는 테스트, 즉 대부분의 버그에 대해서 QA까지 가지 않아도 개발자가 먼저 잡아낼 수 있게 도와주고, 리팩토링에도 건재한 테스트는 무엇일까? 한눈에 의미를 쉽게 파악할 수 있고 유지보수하기에 많은 공수가 들어가지 않는 테스트는 무엇일까?
책을 읽어보자
좀 더 좋은 테스트를 작성할 수 있는 힌트를 얻고자 <Unit Testing Principles, Practices, and Patterns>라는 책을 읽어보려고 한다. 이 책의 내용을 잘 체득한다면 서비스의 품질과 동료 개발자의 생산성을 높이는 데 내가 일조할 수 있지 않을까, 하는 희망을 품고서!
이번 글에서는 이 책의 1장의 내용을 요약하면서, 나의 생각이나 경험을 살짝 덧붙이려고 한다. (대부분 책의 내용이고 내 의견이 아님에도 불구하고 자연스러움을 위해 인용체를 사용하지 않겠다.)
도움이 되는 유닛 테스트를 지향하자
- 모든 테스트가 같은 가치를 가진 것이 아니다. 좋은 테스트도 존재하고, 나쁜 테스트도 존재한다.
- 좋은 테스트는 소프트웨어 프로젝트의 생산성과 품질을 유지할 수 있도록 해준다. 요구사항이 추가되더라도 이를 수용하는 것이 어렵지 않으며, 테스트를 유지보수하는 데 많은 비용이 들어가지 않는다.
- 나쁜 테스트는 소프트웨어 프로젝트의 생산성을 끌어내리고 소프트웨어의 품질을 보장하지 않는다. 나쁜 테스트는 쉽게 깨지고, 유지보수하는 데 많은 고통이 따른다.
- 우리는 적은 비용으로 많은 이익을 얻을 수 있는 테스트만을 유지해야 한다.
유닛 테스트의 목적은 무엇인가
- 유닛 테스트를 작성하는 것이 좋은 디자인을 지향하게끔 도와주는 것은 사실이다. 유닛 테스트를 할 수 있는 코드를 작성해야 하니까. 하지만 이는 유닛 테스트의 좋은 효과일 뿐 목적은 아니다.
- 유닛 테스트를 할 수 없다면 그 코드는 엉망이라는 것을 확신할 수 있지만, 유닛 테스트를 할 수 있다고 해서 그 코드가 좋은 코드라는 보장은 할 수 없다.
- 유닛 테스트의 목적은 소프트웨어 프로젝트의 지속가능한 발전을 가능하게 하는 것이다. 좋은 유닛 테스트는 기능을 추가하거나 리팩토링을 했을 때 생길 수 있는 대부분의 버그(regression)을 잡아준다.
- 즉, 좋은 테스트는 지속가능성(sustainability)와 확장가능성(scalability)을 도와준다. 프로젝트의 초기에 맛볼 수 있는 빠른 생산성을 프로젝트가 많이 진행되어도 유지할 수 있도록 해준다. (우리 조직도 자동화된 테스트를 통해 버그를 잡아내지 못하고 QA에 많은 것을 의존하는 것 같다. 그래서 기능 하나 변경하거나 추가하는 것에 공수가 많이 들어가는 것 같다 ㅠㅠ)
- 나쁜 테스트는 리팩토링에 취약하고(false alarm), 버그(regression)을 잡아내지 못하며, 유지보수하기 힘들다.
- 사람들은 테스트가 많을수록 좋을 것이라고 생각하지만 절대 그렇지 않다. 테스트도 하나의 코드일 뿐이며, 코드가 늘어날수록 잠재적인 버그가 생길 수 있는 부채가 늘어나게 된다. 왜 테스트를 작성하는지 이유를 모른 채 그냥 작성하면 안 된다.
테스트 커버리지를 맹신할 수 없는 이유
- 코드 커버리지(code coverage) = (테스트에 의해 실행된 라인 수) / (전체 코드 라인 수)
- 코드 커버리지는 단순히 라인만 보기 때문에 코드를 컴팩트하게 작성할수록 자동적으로 그 수치가 높아지게 되는 함정이 존재한다.
- 브랜치 커버리지(branch coverage) = (테스트에 의해 실행된 코드 브랜치 수) / (전체 코드 브랜치 수)
- 모든 분기를 다 보기 때문에 브랜치 커버리지가 코드 커버리지보다는 낫지만, 브랜치 커버리지에도 두 가지 함정이 존재한다.
- 모든 발생 가능한 결과를 다 테스트했다는 보장은 없다. (edge case가 있을 수 있다)
- 그 어떤 커버리지 수치도 외부 라이브러리를 고려하지 않는다. (물론 외부 요소는 고려하지 않는 게 맞지만, 모든 요소가 테스트된 것은 아니라는 것)
- 테스트 커버리지는 단순히 그 부분이 '실행되었음'만을 의미한다. 심지어 assert가 전혀 없는 테스트도 커버리지에는 포함된다.
- 이러한 이유로 인해 테스트 커버리지는 맹신할 수 없다. 높은 테스트 커버리지라고 해서 절대 좋은 품질을 보장하는 것은 아니다. 테스트 커버리지는 참고할 만한 지표일 뿐이다.
- 특정한 수치의 테스트 커버리지가 그 자체로 목표가 되어서는 안 된다. 목표치를 넘기기 위해 쓸모없는 테스트를 억지로 만들어내는 데 대부분의 에너지를 낭비하게 만들기 때문이다. (이건 내가 직접 겪어봐서 잘 안다. 대학교 시절 개발 프로젝트를 하는데 교수님이 정해준 테스트 커버리지 목표치가 있었다. 감점을 받지 않기 위해서는 그 수치를 꼭 넘겨야 해서, 별의별 꼼수를 부려가면서 팀원들과 개고생했던 기억이 있다. 그런 테스트들은 커버리지만 겨우 높였을 뿐 우리 서비스에 전혀 도움이 되지 않았다.)
좋은 유닛 테스트란 무엇인가
- 좋은 유닛 테스트를 판별하는 자동화된 기준은 없다. 개인적으로 판단해야 한다.
- 좋은 테스트는 다음과 같은 특징을 가지고 있다.
- 개발 사이클에 통합되어 있다.
- (우리 조직에서는 pull request를 올릴 때마다 전체 테스트를 다 실행하게 자동화가 되어 있는데, 정말 잘하고 있는 것 같다.)
- 프로덕션 코드의 중요한 부분에 집중한다.
- 중요한 부분을 테스트해야 테스트를 작성하는 의미가 있다.
- 보통 중요한 부분은 비즈니스 로직이다(도메인 모델).
- 코어 로직의 테스트 커버리지는 높은 것이 좋다. 하지만 중요하지 않은 부분에까지 높은 커버리지를 요구하는 것은 안 된다. 덜 중요한 부분은 간략하게 테스트하거나 간접적으로 테스트해야 한다.
- 이 원칙을 지키기 위해서는 프로덕션 코드를 작성할 때부터 중요한 부분(도메인 모델)과 아닌 부분(인프라 코드, 외부 의존성 등)을 분리하도록 설계해야 한다. (이 내용은 단순히 테스트 작성뿐만 아니라 코드 디자인에 대해서 생각해볼 수 있는 내용이기 때문에 많은 관심이 간다.)
- 최소한의 비용으로 최대의 가치를 제공한다.
- 유지보수하는 비용보다 제공하는 가치가 월등히 큰 테스트만을 유지해야 한다. (보이스카웃 룰을 실천하면 최대한 좋을 것 같다. 가치가 없는 테스트는 볼 때마다 리팩토링해서 좋게 만들거나 아니면 지워버리기!)
- 개발 사이클에 통합되어 있다.
- 좋은 테스트를 작성하기 위해서는 1. 무엇이 좋은 테스트인지 알아야 하고, 2. 좋은 테스트를 작성할 줄 알아야 한다.
- 1번보다 2번이 훨씬 어렵다. (따라서 이 책에서는 2번을 돕기 위해 코드 디자인에 대해서도 많이 이야기할 것이라고 한다. 기대돼!)
앞으로가 기대되는데?
저자는 1장에서 "좋은 테스트와 나쁜 테스트는 무엇인가?"라는 질문에 대한 개곽적인 답과 함께, 독자가 이 책을 읽는다면 얻을 수 있는 점에 대한 감언이설을 늘어놓았다. 많은 관심을 가지고 저자의 메시지를 들어보려고 한다:)