Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

테스트는 얼마나 해야 적당할까? 효율적인 테스트 전략의 세 가지 원칙

잘못된 테스트로 가득한 코드를 다뤄본 적이 있다면 그 고통을 잘 알 것입니다. 버그 하나를 고칠 때마다 새로운 버그 다섯 개가 생기고, 코드가 제대로 동작하더라도 그것이 설계 의도에 따른 결과인지, 아니면 우연히 맞아떨어진 것인지 확신할 수 없기 때문입니다.

반대로, 아주 작은 기능 하나를 배포하기 위해 무려 200개에 달하는 테스트를 작성해야 하는 경우도 있습니다. 100% 테스트 커버리지를 채우려고 이미 잘 동작하는 코드를 계속 다시 설계해야 하고, 테스트를 가장 꼼꼼히 작성한 코드가 오히려 더 읽기 어려워진다는 느낌이 듭니다. 최악은 자신의 앱에 대해 번아웃이 오기 시작한다는 점입니다.

분명 중간 지점이 있을 것입니다. 그렇다면 테스트는 얼마나 하는 것이 적당할까요?

"앱 코드의 두 배 분량의 테스트 코드를 작성한다", "95% 테스트 커버리지를 유지한다" 같은 간단한 규칙이 있다면 좋겠지만, "95% 커버리지"라는 말 자체도 모호합니다.

커버리지는 잘 테스트된 코드의 지표일 수는 있지만, 잘 테스트된 코드의 보장은 아닙니다. 실제로 커버리지 100%짜리 앱이 커버리지 85%짜리 앱보다 더 많은 버그를 가진 경우를 경험한 적도 있습니다.

따라서 적절한 테스트의 양은 숫자로 정해질 수 없습니다. 대신 좀 더 추상적이고 정의하기 어려운 무엇과 관련이 있는데요. 바로 효율적으로 테스트하는 것입니다.

효율적인 테스트란?

효율적인 테스트란 가장 적은 노력으로 가장 큰 효과를 얻는 것입니다. 들어만 해도 매력적이죠?

하지만 테스트를 더 효율적으로 만들기 위해서는 고려해야 할 요소가 많습니다. 특히 세 가지를 생각해 보면 도움이 됩니다. 바로 크기(size), 격리(isolation), 집중(focus)입니다.

1. 크기(Size)

통합 테스트(integration test)는 훌륭합니다. 실제 사용자가 앱에서 거치는 경로를 그대로 반영하고, 실제 환경에서 사용되는 방식 그대로 여러분의 모든 코드가 함께 동작하는 모습을 검증합니다.

하지만 통합 테스트는 느립니다. 길고 복잡해지기 쉽고, 시스템의 작은 부분 하나를 철저히 테스트하고 싶을 때는 상당한 오버헤드가 됩니다.

유닛 테스트(unit test)는 더 작고 빠르게 실행됩니다. 작성할 때 시스템의 아주 작은 부분만 머릿속에 담으면 되기 때문에 이해하기도 쉽습니다.

하지만 유닛 테스트는 '가짜'일 수 있습니다. 유닛 테스트 안에서 동작한다고 해서 실제 환경에서도 동작한다고 장담할 수 없기 때문입니다. 특히 목(mocking)을 많이 사용할 경우 더욱 그렇습니다.

그렇다면 어떻게 균형을 잡아야 할까요?

유닛 테스트는 빠르고 작성하기 쉬우므로, 많은 수의 테스트를 유지하는 비용이 크지 않습니다. 따라서 엣지 케이스나 복잡한 로직을 검증하기에 가장 적합합니다.

시스템의 구성 요소들을 충분히 테스트했다면, 남은 공백을 메워야 합니다. 즉, 각 부분이 서로 어떻게 상호작용하는지, 사용자가 앱에서 거칠 수 있는 전체 흐름을 테스트해야 합니다. 하지만 대부분의 엣지 케이스와 로직은 이미 유닛 테스트로 검증했기 때문에, 이처럼 복잡하고 느린 통합 테스트는 몇 개만 있으면 충분합니다.

이 개념을 "테스트 피라미드(Test Pyramid)"라고 부릅니다. 많은 유닛 테스트를 기반으로 하고, 그 위에 소수의 통합 테스트가 올라가는 형태입니다.

2. 격리(Isolation)

시스템이 복잡하다면 발생 가능한 모든 상황을 커버하기 위해 사실상 무한대에 가까운 테스트가 필요할 수 있습니다. 이것은 앱 설계를 다시 생각해 봐야 한다는 신호일 수 있습니다. 시스템의 일부분들이 서로에게 지나치게 강하게 결합되어 있다는 뜻이니까요.

예를 들어 객체가 몇 가지 서로 다른 상태 중 하나일 수 있다고 가정해 보겠습니다.

case user.type
when :admin
  message = admin_message
when :user
  message = user_message
when :author
  message = author_message
else
  message = anonymous_message
end

if user.preferred_notification_method = :email
  send_email(message)
elsif user.preferred_notification_method = :text
  send_text_message(message)
else
  queue_notification(message) 
end

여기서 모든 경로를 테스트하려면 12가지 상황을 검증해야 합니다.

  1. 관리자(admin) + preferred_notification_method가 email
  2. 관리자(admin) + preferred_notification_method가 text
  3. 관리자(admin) + 둘 다 아님
  4. 일반 사용자(user) + preferred_notification_method가 email
  5. 일반 사용자(user) + preferred_notification_method가 text
  6. 일반 사용자(user) + 둘 다 아님
  7. 작성자(author) + preferred_notification_method가 email
  8. 작성자(author) + preferred_notification_method가 text
  9. 작성자(author) + 둘 다 아님
  10. 익명(anonymous) + preferred_notification_method가 email
  11. 익명(anonymous) + preferred_notification_method가 text
  12. 익명(anonymous) + 둘 다 아님

테스트해야 할 케이스가 이렇게 많은 이유는 "알림 방식에 따라 메시지 보내기"와 "사용자 유형에 따라 메시지 생성하기"가 서로 얽혀 있기 때문입니다. 테스트 수를 줄일 방법이 없지는 않겠지만 명확하지 않으며, 그대로 두었다가는 버그를 불러들이기 십상입니다.

하지만 이 두 가지를 분리하면 어떻게 될까요?

message = get_message_based_on_user_type(user.type)

send_notification(message, user.preferred_notification_method)

이제 각 부분을 독립적으로 테스트할 수 있습니다.

첫 번째 부분에서는 각 사용자 유형마다 올바른 메시지가 반환되는지 테스트하고, 두 번째 부분에서는 주어진 메시지가 preferred_notification_method 값에 따라 올바르게 전송되는지 테스트합니다. 마지막으로, 상위 메서드가 반환된 메시지를 알림 전송 함수로 제대로 전달하는지 테스트하면 됩니다. 이렇게 하면 테스트해야 할 상태가 8개로 줄어듭니다.

  1. 사용자가 관리자
  2. 사용자가 일반 사용자
  3. 사용자가 작성자
  4. 익명 사용자
  5. preferred_notification_method가 email
  6. preferred_notification_method가 text
  7. preferred_notification_method가 둘 다 아님
  8. 상위 메서드에 대한 테스트 1건

이처럼 코드를 분리해서 개별적으로 테스트할 수 있게 만들면 테스트 4개를 절약할 수 있습니다. 두 번째 예시에서는 테스트 수를 줄일 수 있다는 사실이 훨씬 명확하게 드러납니다. 그리고 상태가 더 추가될수록 코드를 나누는 것이 훨씬 더 유리해집니다.

격리와 가독성 사이의 최적의 균형점을 찾는 데에는 시간과 연습이 필요합니다. 하지만 의존성을 올바른 위치에서 끊어낼 수 있다면, 훨씬 적은 수의 테스트로도 충분히 커버할 수 있습니다.

3. 집중(Focus)

앱은 잘 테스트되어야 합니다. 하지만 앱의 모든 부분이 똑같은 수준의 테스트 관심을 받아야 한다는 뜻은 아닙니다.

100% 테스트 커버리지를 목표로 한다 해도, 결국 모든 것을 테스트할 수는 없습니다. 예를 들어 화면에 표시되는 모든 문구를 테스트하거나, 업데이트 폴링이 10초가 아닌 5초마다 이루어지는지까지 검증하지는 않을 것입니다.

바로 여기서 '집중'이 필요합니다. 더 적지만 더 유용한 테스트를 작성하고, 한정된 시간을 어디에 쓰는 것이 가장 좋은지 의식적으로 결정하는 것입니다.

집중 역시 쉽게 잡기 어려운 영역입니다. 가장 중요한 테스트에 집중하기 위해 스스로에게 던져볼 만한 질문들은 다음과 같습니다.

  • 이 부분은 앱의 나머지 부분과 얼마나 밀접하게 연결되어 있는가? 만약 깨진다면 몇 개의 다른 기능이 함께 무너지는가?

  • 이 부분이 자연스럽게 변경될 가능성은 얼마나 되는가? 테스트 실패의 원인이 진짜 버그인가, 아니면 누군가 UI 문구를 수정한 것 때문인가?

  • 이 부분이 깨졌을 때 파급력은 어느 정도인가? 누군가의 신용카드에 결제가 두 번 되는 문제인가, 아니면 단순히 문구가 빠지는 정도의 문제인가?

  • 이 부분은 얼마나 자주 사용되는가? 앱의 핵심 동작에 필수적인가, 아니면 푸터 깊숙한 곳에 숨어 있는 소개 페이지 정도인가?

중요한 부분 테스트하라는 뜻은 아닙니다. 하지만 테스트 시간을 현명하게 쓰면, 실제로 더 높은 품질처럼 느껴지는 앱을 만들 수 있습니다.

결국, 균형이 핵심이다

사용자가 앱에서 거칠 수 있는 모든 경로를 전부 테스트하려고 들면, 끝내 출시할 수 없습니다. TDD는 도움이 되지만, 테스트와 관련된 모든 문제를 해결해 주지는 못합니다.

물론, 그렇다고 아예 테스트를 하지 말라는 뜻은 아닙니다.

테스트 피라미드를 활용해 테스트를 작게 유지할 수 있습니다. 의존성을 분리해서 m * n개의 테스트 케이스를 m + n개로 줄일 수 있습니다. 그리고 우선순위를 정해 앱에서 가장 중요한 부분에 더 많은 시간을 투자할 수 있습니다.

그래서, 여러분은 테스트를 얼마나 하시나요? 앱을 만들 때 이런 개념들을 고려하시나요? 그리고 앱의 어떤 부분에 집중해야 할지는 어떻게 판단하시나요? 댓글로 여러분의 경험을 들려주세요!