원문: High Cost Tests and High Value Tests by @Noel Rappin


img

테스트의 비용과 효용 측정에 장표는 필요하지 않을 겁니다.

(이 글은 이메일로 진행되는 Noel Rappin의 Testing Journal 수업의 추가 자료입니다. 이메일 수업의 내용과 관련은 있으나 완전히 들어맞지는 않아서 따로 작성합니다. 당신이 이 글을 괜찮게 여기신다면 제 수업에도 가치를 느끼실 겁니다. 제가 Justin Searls, Sam Phippen과 함께 진행하는 Tech Done Right 팟캐스트에서도 비슷한 주제로 논의했던 에피소드를 찾아보실 수 있습니다.)

저는 종종 테스트를 “고비용” 또는 “고효용”으로 묘사합니다. 물론 당신은 대체 무엇이 테스트의 효용을 높이는건지 궁금하실 겁니다. 테스트를 금이나 다른 물건으로 교환할 수 있는 시장 따위는 존재하지 않으니까요.

한번 파고들어가 보죠. 테스트에는 비용과 효용이 존재합니다.

하나의 테스트에 대한 비용은 다음 목록을 포함합니다:

  • 테스트 작성에 드는 시간
  • 매 테스트 스위트마다 테스트 실행에 걸리는 시간
  • 테스트를 이해하는 데 걸리는 시간
  • 코드가 괜찮지만 테스트는 깨질 때 테스트 수정에 드는 시간
  • 어쩌면, 테스트가 가능하도록 코드를 변경하는 시간

여기서 비용의 단위는 시간입니다. 테스트에는 시간이 듭니다. 모든 프로그래밍의 의사결정은 결국 시간의 문제입니다. 코드 품질 때문에 변경이 쉬워지거나 어려워진다는 말도 사실 코드 변화에 드는 시간에 대한 이야기죠.

테스트에 시간이 든다면, 테스트로 시간을 벌 수도 있을까요?

  • 테스트 작성 행위가 코드 구조를 잡기 더 쉽게 해줍니다.
  • 자동화 테스트는 수동으로 복잡한 통합 단계를 일일이 거치는 것보다 빠릅니다.
  • 테스트는 코드가 의도한 대로 동작하고 있음을 효율적으로 증명해줍니다.
  • 테스트는 코드 변화로 인해 동작 변화가 생겼음을 경고해줍니다.

테스트의 비용과 효용은 코드의 전체 생애주기에 걸쳐 나타납니다. 처음 테스트가 작성됐을 때 비용이 생기고, 테스트가 읽히고 실행될 때마다 비용이 점차 누적되죠. 만약 테스트가 실패했는데 실제 코드에는 문제가 없을 때에는 큰 비용이 생깁니다. 어설픈 테스트가 시간을 크게 잡아먹을 수 있다는 겁니다.

테스트가 당신이 정확한 코드를 더 빠르게 작성할 수 있도록 (TDD 테스트처럼) 돕는다면, 그 테스트는 작성됐을 때부터 효용을 더해줍니다. 수동 테스트 대신 자동 테스트를 돌릴 때에도 효용이 생기고요. 새 코드를 만들었을 때 테스트가 실패함으로써 무언가 문제가 생겼음을 알려준다면 크게 시간이 절약됩니다.

Shoulda gem을 예로 들어보죠. 이 gem은 should belong_to :users 와 같은 여러 matcher를 제공하는데, 그게 테스트의 전부입니다. 이 테스트는 비용이 낮습니다. 순식간에 작성할 수 있고, 이해하기 쉽고, 빨리 실행되니까요. 하지만 동시에 효용도 낮습니다. 코드 로직이 아닌 데이터베이스에 종속되어 있어서 코드의 디자인에 대해 테스트가 설명해주는 것이 별로 없습니다. 이 테스트 혼자 실패할 일도 거의 없죠. users association이 없어진다면 아마 수많은 테스트가 실패할 테니, 이 테스트가 실패했다는 걸 앎으로써 당신이 새롭게 알게 되는 사실은 없을 겁니다.

반대로, 전체 결제 프로세스를 훑도록 Capybara로 만들어진 E2E 통합 테스트를 생각해봅시다. 이 테스트는 고비용일 겁니다. 수많은 데이터와 여러 테스트 단계를 거칠 것이고 결과 매칭도 복잡할 테니까요. 또한, 이 테스트는 고효용일 겁니다. 어쩌면 결제 과정에서 모든 다른 조각들이 제대로 통신하면서 동작하는지 확인할 수 있는, 즉 다른 테스트로는 찾아낼 수 없는 에러를 발견하는 유일한 테스트가 이녀석일지도 모르죠. 그리고 개발 후에 결제를 한단계씩 거치며 직접 해보는 것보다는 테스트 실행이 빠를 겁니다.

즉 오랜 시간동안 당신의 테스트 스위트를 행복하게 만들려면, 테스트의 비용은 최소화하고 효용은 극대화해야 합니다. 뻔한 말로 들리실 수 있겠지만, 명시적으로 비용과 효용의 측면에서 생각해보는 건 도움이 됩니다.

몇 가지 구체적인 조언을 드려보자면…

  • 테스트 통과보다는 실패에 집중하세요. 테스트를 실패시킬 방법이 없다면, 불필요한 테스트일지도 모릅니다.
  • 통합 테스트를 염두에 두고 개발하면 자잘한 스텝들을 자동화하기 쉽습니다.
  • 단위 테스트는 테스트가 실패하게 만들 최소한의 코드로만 작성하세요. 예를 들면, 에러 케이스들은 느리고 복잡한 통합 테스트보다는 테스트 대역과 함께 좁게 집중된 단위 테스트로 만드는 게 낫습니다.
  • 단위 테스트가 실제 의존성을 사용(외부 라이브러리 호출, 데이터베이스에 많은 데이터 저장 등)하지 않도록 테스트 대역을 쓰세요.
  • 버그 하나가 여러 테스트를 실패하게 만든다면 그 모든 테스트가 전부 필요한지 생각해 보세요. 실패가 셋업 단계에 있다면, 그 모든 테스트가 다 그 셋업을 필요로 하는지 생각해보시고요.
  • 때로, 개발 단계에서 유용했던 한 테스트가 나중에 만든 다른 테스트에 완전히 포함될 수 있습니다. 리팩토링하세요.

테스트를 작성할 때 단기적 / 장기적 비용을 고려하면 테스트가 더 좋아집니다. 그리고 테스트가 좋아지면 코드도 좋아지죠.

감사합니다.