박종훈 기술블로그

테스트 코드를 작성해야 하는 이유와 테스트 설계하기

구글 엔지니어는 이렇게 일한다
11장 테스트 개요


개발자 주도 테스트, 자동 테스트

자동 테스트(automated test) 는 버그로 인해 고객이 놀라는 사태를 막아줍니다. 또한 소프트웨어가 변화할 수 있도록 지원 합니다. 새로운 기능을 추가하거나, 코드가 더 건실해지도록 리팩터링하거나, 대규모 재설계를 진행하는 상황에서 자동 테스트는 실수를 빠르게 잡아주므로 안심하고 소프트웨어를 변경할 수 있게 해줍니다.

반복 주기가 짧은 회사는 변화하는 기술, 시장 상황, 고객 취향을 더 빠르게 받아들일 수 있습니다. 테스트 체꼐가 잘 갖춰져 있다면 변화를 두려워할 이유가 없습니다. 따라서 테스트 체계를 소프트웨어 개발의 핵심 역량으로 취급할 수 있습니다. 시스템을 더 많이 더 빠르게 변경하고 싶다면 더 빠르게 테스트하는 방법을 모색해야 합니다.

테스트를 작성하는 행위는 시스템의 설계도 개선해줍니다. 테스트는 시스템 코드의 첫번째 고객이라는 자격으로 여러분이 설계한 설계에 관해 많은 이야기를 해줍니다. 데이터베이스에 너무 강하게 묶여 있는지. 이 API가 필수 유스케이스를 지원하는지, 시스템이 극단적인 상황들에 잘 대처하는지 등을 확인할 수 있습니다. 자동 테스트를 작성하면 이런 문제를 개발 주기의 초반에 잡아내게 됩니다. 그 결과 모듈화가 더 잘되어 미래의 변화에 훨씬 유연한 소프트웨어가 만들어집니다.

소프트웨어 테스트가 중요하다는 인식이 퍼지면서 테스트를 주제로 하는 문헌이 많이 쏟아졌습니다. 하지만 어떻게 해야 테스트를 잘할 수 있는지는 여전히 많은 이에게 의문으로 남아있습니다.

11.1 테스트를 작성하는 이유

자동 테스트 라고 하는 것은 정확히 무엇일까요?

테스트는 다음 요소들로 정의할 수 있습니다.

  • 테스트하려는 단 하나의 행위
  • 특정한 입력
  • 관측 가능한 출력 혹은 동작
  • 통제된 조건 (하나의 격리된 프로세스 등)

이런 테스트를 실행한다면 여러분은 시스템에 특정 값을 입력하고, 출력 결과를 확인하여, 시스템이 기대한 대로 동작한 것인지를 판단하게 될 것입니다.

그리고 간단한 테스트가 수백에서 수천 개가 모이면 (테스트 스위트, test suite) 제품이 전체적으로 의도한 설계대로 잘 작동하는지, 혹은 언제 그렇지 못한 지를 이야기할 수 있게 됩니다 (테스트에서는 후자가 더 중요)

테스트 스위트, test suite
여기서 suite 는 세트(모음) 를 의미한다.

a test suite is a collection of test cases that are intended to be used to test a software program to show that it has some specified set of behaviors - wikipedia 테스트 스위트는 특정 동작 세트가 있음을 보여주기 위해 소프트웨어 프로그램을 테스트하는 데 사용되는 테스트 사례 모음입니다.

테스트 스위트를 건실하게 만들고 유지하는 데는 많은 노력이 듭니다. 코드베이스의 덩치에 비례하여 테스트 스위트도 커집니다. 그 과정에서 테스트 겨로가가 일관되지 못하거나 느려지는 문제가 나타나기도 합니다. 이러한 문제를 해결하지 못하면 테스트 스위트의 존폐가 위태로워집니다. 테스트는 엔지니어에게 신뢰를 줄 때만 가치가 있습니다. 테스트가 생산성을 떨어뜨리고 고칠 게 계속 나오거나 결과를 믿을 수 없다면 엔지니어들은 더 이상 테스트를 신뢰하지 못하고 우회 방법을 찾으려 할 것입니다. 나쁜 테스트 스위트는 테스트가 아예 없는 것만 못합니다.

11.1.1 구글 웹 서버 이야기

구글은 테스트를 엔지니어링 문화의 중심에 두고 있습니다. 하지만 구글도 초창기에는 엔지니어 주도 테스트를 크게 중요하게 여기지 않았습니다. 똑똑한 엔지니어들이 소프트웨어를 알아서 올바르게 만들어줄 것이라 기대한 것이죠.

GWS는 구글 검색 쿼리를 제공하는 웹 서버입니다. 구글에서 매우 중요한 서버입니다. 이 서버의 규모와 복잡성이 커지면서 생산성이 급격히 떨어졌습니다. 버그가 넘쳐났고 릴리스까지의 기간도 점점 길어졌습니다. 개발자들은 기능 수정을 하려면 불안해했고 프로덕션 환경에서 기능이 작동하지 않아 롤백도 빈번하게 진행해야 했습니다.

이러한 문제를 해결하고자 GWS의 테크 리드(TL)는 엔지니어 주도의 자동 테스트를 정책 차원에서 도입하기로 했습니다. 이 정책의 일환으로 모든 코드 변경에는 지속해서 실행할 수 있는 테스트가 반드시 딸려 있어야 했습니다. 정책 도입 1년 만에 긴급하게 코드를 수정해 배포하는 건수가 절반으로 줄었습니다.

테스트는 구글에서 가장 중요한 프로젝트 중 하나인 GWS의 생산성과 자신감을 높여줬습니다.

현재 GWS는 수만 개의 테스트를 보유하고 있으며 고객이 인식할 수 있는 버그의 수를 상대적으로 낮게 억제하면서 거의 매일 릴리스되고 있습니다.

이 사례는 구글의 테스트 문화를 격변시키는 분수령이 되었습니다. 다른 팀들도 비슷한 전략을 도입하기 시작하였습니다.

이 경험에서 배운 핵심은 제품 결함 해결을 프로그래머의 능력에만 의존해서는 안된다는 사실이였습니다. 개별 엔지니어가 버그를 심는 빈도는 아주 낮더라도 팀원이 많아지면 결함 목록은 계속 길어질 것입니다. 설상가상으로 복잡한 시스템에서는 버그를 고치는 과정에서 다른 버그를 만들게 되기도 합니다.

최고의 팀은 팀원들의 집단 지성을 팀 전체의 이익으로 환원하는 방법을 찾아냅니다. 개별 엔지니어가 작성한 테스트는 팀이 공유하는 자원 풀에 추가됩니다. 따라서 팀원 모두가 공유된 테스트를 수행하고 결함을 찾아낼 수 있습니다.

디버깅 방식에서는 버그가 발생할 때마다 엔지니어가 디버거를 실행해 문제를 분석해야 합니다. 둘 사이의 엔지니어링 비용 차이는 하늘과 땅만큼 벌어지게 됩니다.

11.1.2 오늘날의 개발 속도에 맞는 테스트

소프트웨어 시스템은 계속해서 더 커지고 복잡해지고 있습니다. 코드도 더 커지고, 환경도 더 다양해지고, 릴리즈도 더 자주합니다.

모든 기능을 사람이 조작하여 제품 품질을 확인하는 방식은 확장이 불가능합니다. 그래서 테스트에서는 해법은 단 하나 ‘자동화’ 뿐입니다.

11.1.3 작성하고, 수행하고, 조치하라

가장 순수한 형태의 자동 테스트는

  • 테스트 작성
  • 테스트 수행
  • 실패한 테스트엥 대한 조치

이렇게 세 가지 활동으로 이루어집니다.

자동 테스트는 적은 양의 코드로 구성됩니다.

  • 테스트 코드는 환경을 원하는 모습으로 설정하고
  • 시스템을 호출하고
  • 결과를 검증합니다.

하나의 코드 실행 경로만 확인하는 아주 작은 테스트도 있고 모바일 운영체제나 웹 브라우저 같은 전체 시스템이 필요한 커다란 테스트도 있습니다.

전문 소프트웨어 테스트들이 시스템의 새 버전이 나올 때마다 모든 기능을 열심히 시험하며 검수하던 과거의 품질보증(QA, quality assurance) 프로세스와 달리
오늘날의 개발자들은 자신의 코드를 검사하는 자동 테스트를 작성하고 수행하는 데 능동적이고 핵심적인 역할을 합니다.

오늘날 시스템의 규모와 배포 속도를 따라잡으려면 모든 엔지니어가 테스트도 함께 개발해야만 합니다.

물론 테스트를 작성하는 것과 좋은 테스트를 작성하는 것은 별개입니다. 수만 명의 엔지니어에게 좋은 테스트를 작성하도록 가르치기는 정말 어렵습니다. 우리가 구글에서 배운 ‘좋은 테스트 작성법’은 이어지는 3개 장(12~14장)에서 살펴보겠습니다.

  • 12장: 단위 테스트
  • 13장: 테스트 대역
  • 14장: 더 큰 테스트

테스트 작성은 테스트 자동화 프로세스의 첫 번째 단계일 뿐입니다. 테스트를 작성한 후에는 작성한 테스트를 수시로 실행해야 합니다. 자동 테스트의 핵심은 같은 동작을 끊임없이 반복하는데 있습니다. 절차에 맞춰 수동으로 진행하던 과거의 테스트에서 벗어나 테스트 절차까지 ‘실행 가능한 코드’로 작성해두면 코드가 변경될 때마다 테스트를 돌려볼 수 있습니다.

지속적 통합과 테스트는 23장에서 설명합니다.

테스트를 코드로 작성하면 다양한 환경에서 수행할 수 있도록 테스트들을 ‘모듈화’하기도 좋습니다. (브라우저, 언어 등 바뀌는 부분이 있어도 이상적으로 최종 사용자가 얻는 경험은 모두에게 똑같아야 함)

테스트 프로세스가 얼마나 효과적이냐는 테스트 실패를 어떻게 처리하느냐에 달려 있습니다. 실패하는 테스트가 해결되지 모샇고 빠르게 쌓여간다면 테스트에 투자한 노력이 허사가 되니, 그렇게 되지 않도록 하는 게 중요합니다. 테스트를 빠르게 해결하는 팀이라면 제품과 테스트를 더 신뢰하며 오류를 빠르게 퇴치할 수 있습니다.

요약하면 건실한 자동 테스트 문화에서는 모두가 테스트를 작성하고 공유하도록 장려합니다. 테스트를 정기적으로 실행합니다. 가장 중요한 것은 테스트가 실패하면 바로 조치하도록 권장해야 테스트 프로세스를 신뢰하고 계속 이어나갈 수 있습니다.

11.1.4 테스트 코드가 주는 혜택

테스트 문화가 확실하게 뿌리 내린 조직을 경험해보지 못한 개발자는 테스트를 작성하면 생산성과 속도가 높아진다고는 생각하기 어려울 것입니다. 오히려 반대로 생각할 가능성이 큽니다. 처음에는 기능 구현에 드는 시간을 테스트 작성에도 써야 하니깐요.

테스트 코드가 주는 혜택은 다음과 같습니다.

디버깅 감소 테스트를 거친 후 서브밋되는 코드는 통상적으로 결함이 적습니다. 테스트를 한 번 작성해두면 값비싼 결함을 예방해주고 짜증 하는 디버깅에서 해방시켜줍니다.

자신 있게 변경 모든 소프트웨어는 변경됩니다. 좋은 테스트들로 무장한 팀은 자신감을 가지고 변경들을 리뷰하고 수용할 수 있습니다. 행위가 달라지지 않는 변경 (리팩터링) 은 기존 테스트를 수정할 필요조차 없습니다.

더 나은 문서자료 소프트웨어 문서자료는 신뢰할 수 없는 것으로 악명 높습니다. 문서자료가 해당 코드를 잘 대변하지 못하는 일이 다반사입니다.

명확한 테스트는 마치 실행 가능한 문서와 같습니다. 코드가 특정 상황에서 어떻게 동작하는지 궁금하다면 그 상황을 검증하는 테스트를 보면 됩니다. 요구사항이 변경되어 새로운 코드가 기존 테스트를 통과하지 못한다면 그 ‘문서자료(테스트)’가 이제 낡았음을 분명히 알 수 있습니다.

테스트를 명확하고 간결하게 작성해야 문서자료로서의 역할을 훌륭히 수행할 수 있습니다.

더 단순한 리뷰 구글에서는 최소 한 명의 다른 엔지니어가 리뷰한 코드만이 서브밋될 수 있습니다. 이때 정확성, 극단 상황, 오류 상황 등 다양한 측면에서 코드를 검사해주는 테스트가 주닙되어 있다면 리뷰어가 변경된 코드가 제대로 작동하는지를 검증하는 시간을 크게 줄여줍니다.

사려 깊은 설계 새로 작성한 코드의 테스트를 작성하는 일은 실질적으로 해당 코드의 API가 잘 설계되었는지를 시험하는 행위입니다. 테스트하기 어려운 코드는 너무 많은 역할을 짊어지거나 의존성을 관리하기 어렵게 짜여있기 때문일 가능성이 큽니다. 잘 설계된 코드라면 모듈화가 잘 되어 있어야 합니다.

고품질의 릴리스를 빠르게 건실한 자동 테스트 스위트를 갖춘 팀은 새로운 버전을 릴리스하며 불안에 떨지 않습니다.

11.2 테스트 스위트 설계하기

엔지니어들은 커다란 ‘시스템 규모’의 테스트를 작성하는 편을 선호하지만, 작은 테스트가 더 빠르고, 안정적이고, 평균적으로 고통이 적습니다.

작다는 것의 정확한 의미를 찾기까지는 많은 논의가 필요했습니다. ‘작다’는 것은 단위 테스트를 의미하는 것일까요? 통합 테스트에서 ‘작다’라는건 무슨 뜻일까요? 등등

모든 테스트 케이스에는 두 가지 독립된 요소가 있습니다. 크기와 범위입니다. 크기(size) 는 테스트 케이스 하나를 실행하는 데 필요한 자원을 뜻입니다. 메모리, 프로세스, 시간 등이죠. 한편 범위(scope) 는 검증하려는 특정한 코드 경로(code path)를 뜻합니다.

11.2.1 테스트 크기

구글에서는 모든 테스트를 크기 기준으로 분류하며, 엔지니어들에게 주어진 기능 조각을 검사하는 가능한 한 작은 테스트를 작성하라고 독려합니다.

테스트의 크기를 가늠하는 기준은 코드 줄 수가 아닙니다. 대신 어떻게 동작하고, 무엇을 하고, 얼마나 많은 자원을 소비하는지로 평가합니다.

작은 테스트는 프로세스 하나에서 동작하고, 중간 크기 테스트는 기기 하나에서, 큰 테스트는 자원을 원하는 만큼 사용해 동작합니다.

test size

구글은 전통적인 용어인 ‘단위 테스트’와 ‘통합 테스트’대신 이 정의를 사용합니다. 우리가 테스트 스위트에 바라는 품질은 바로 속도와 결정성이기 때문입니다. (테스트 하는 범위와는 무관합니다.)

작은 테스트는 더 많은 인프라나 자원을 사용하는 테스트보다 거의 항상 더 빠르고 더 결정적입니다. 또 테스트를 만들기도 훨씬 쉽습니다.

테스트가 커질수록 더 유연하지만 비결정적일 위험이 커집니다. 그래서 큰 테스트는 가장 복잡하고 검증하기 어려운 시나리오에만 제한적으로 활용합니다.

작은 테스트

세 가지 테스트 중 작은 테스트는 제약이 가장 엄격합니다. 가장 중요한 제약은 바로 테스트가 단 하나의 프로세스에서 실행되어야 한다는 것입니다.

서버를 두고 독립된 테스트 프로세스에 연결해 수행하거나 데이터베이스와 같은 제3의 프로그램을 수행해서도 안 됩니다.

작은 테스트는 sleep, I/O 연산 같은 블로킹 호출을 사용해서는 안 됩니다. 네트워크와 디스크에도 접근할 수 없다는 뜻입니다. 그래서 블로킹 호출을 수반하는 대상을 검사하는 테스트 코드는 테스트 대역을 사용해야합니다.

이러한 제약들의 목적은 테스트를 느려지게 하거나 비결정적으로 만드는 주요 원인들로부터 작은 테스트를 떼어 놓는 것입니다. 하나의 프로세스에서 실행되고 블로킹 호출을 전혀 하지 않는 테스트라면 CPU가 수행할 수 있는 최고 속도로 실행됩니다. 이런 테스트라면 실수로 느리게 혹은 비결정적으로 만들기 어렵습니다. 이 제약들이 엔지니어의 실수를 막아주는 보호막 역할을 해줍니다.

제약이 너무 과하다고 느껴진다면 작은 테스트 케이스 수백 개로 이루어진 스위트가 하루 종일 실행되는 환경을 생각해보세요. 그중 불규칙한 테스트(flaky test)가 단 몇 개만 있어도 원인을 찾아 헤매느라 생산성이 급격하게 떨어질 것입니다. 구글 규모에서는 이런 문제 하나가 테스트 인프라 전체를 중단시킬 수도 있습니다.

구글은 테스트하는 범위와 상관없이 가능하면 언제나 작은 테스트를 작성하라고 권합니다. 전체 테스트 스위트를 빠르고 안정되게 만들어주기 때문이죠.

중간 크기 테스트 중간 크기 테스트는 여러 프로세스와 스레드를 활용할 수 있고, 로컬 호스트로의 네트워크 호출 같은 블로킹 호출도 이용할 수 있습니다. 단, 외부 시스템과의 통신은 여전히 불허합니다. 한 대의 기기에서 수행되어야 합니다.

여러 프로세스를 사용할 수 있게 되면 할 수 있는 일이 크게 늘어납니다. 예를 들어 데이터베이스 인스턴스를 실행할 수 있게 되어 테스트 대상 코드를 더 현실적인 설정 하에서 검증할 수 있습니다. 또한 웹 UI와 서버 코드의 조합도 테스트할 수 있습니다.

유연성이 커지면 반드급부로 테스트는 느려지고 비결정적이 될 가능성이 높아집니다. 외부 요인이 개입되므로 성능과 결정성을 온전히 우리 스스로가 보장할 수 없다는 뜻입니다. 중간 크기 테스트는 원격 기기로의 네트워크 호출을 불허함으로써 여전히 어느 정도의 보호막은 유지하고 있습니다. 원격 호출은 대부분의 시스템에서 속도를 떠러뜨리고 비결정성을 높이는 가장 독보적인 원흉입니다.

큰 테스트 큰 테스트는 중간 크기 테스트를 구속하던 로컬 호스트 제약에서 해방되어, 테스트와 대상 시스템이 여러 대의 기기를 활용할 수 있게 됩니다.

이번에도 더 유연해지는 만큼 위험도 늘어납니다. 그래서 구글은 큰 테스트를 몇 가지 용도에 한정해서 활용합니다. 하나는 전체 시스템의 종단간(end-to-end) 테스트입니다. 종단간 테스트는 코드 조각이 아닌 설정을 검증하는게 주된 목적입니다.

구글은 큰 테스트를 작은 테스트나 중간 크기 테스트와 분리하여, 빌드나 릴리스 때만 수행되도록 하여 개발 워크플로에 영향을 주지 않도록 합니다.

사례 연구 : 불규칙한 테스트는 비싸다. 테스트가 비결정적이라면 이따금 실패하는 일이 드물지 않습니다. 테스트 수가 많아질수록 통계적으로 실패를 경험할 가능성이 커집니다. 테스트 결과가 불규칙한 상황이 계속되면 생산성이 저하되기도 하지만 테스트를 믿지 못하는 상황을 겪게 될수도 있습니다. 이 단계에 들어서면 엔지니어들은 테스트가 실패하더라도 더는 신경 쓰지 않습니다. 테스트 스위트가 아무런 가치도 만들어내지 못하게 됩니다. 대부분의 불규칙한 실패는 테스트 자체에 비결정적으로 동작하는 로직이 있어서 발생합니다. 좋은 자동 테스트 인프라라면 이러한 갖가지 비결정적인 동작을 엔지니어가 쉽게 파악하고 완화하는 데 도움을 줘야 합니다.

테스트 크기와 무관한 공통 특성

모든 테스트는 밀폐(hermetic) 되어야 합니다. 즉, 셋업, 실행, 테어다운(tear down) 하는 데 필요한 모든 정보를 담고 있어야 합니다. 또한 테스트 수행 순서 같은 외부 환경에 관해서는 가능한 아무것도 가정하지 않아야 합니다. 예를 들어 공유 데이터베이스에 의존해서는 안 됩니다. 이런 제약 때문에 더 큰 테스트를 작성하기가 더 어려워지지만, 그럼에도 테스트 각각을 격리하려는 노력을 멈추면 안됩니다.

테스트는 확인하려는 행위를 수행하는 데 필요한 정보 ‘만’ 을 포함해야 합니다. 테스트가 깔끔하고 간결하면 테스트가 의도대로 동작하는지 검증해야 하는 리뷰어의 어깨를 가볍게 해줍니다. 깔끔한 코드는 테스트 실패 원인을 진단하는 데도 도움이 됩니다. 테스트는 무엇을 검사하는지가 명확해야 합니다.

테스트에서는 조건문이나 순환문 같은 제어문을 쓰지 않는 게 좋습니다. 복잡한 테스트일수록 버그가 숨어들 가능성이 커지며, 실패 원인을 찾기도 어려워집니다.

11.2.2 테스트 범위

구글은 테스트 크기를 많이 강조하지만 테스트 범위도 중요하게 생각합니다. 테스트 범위(test cope) 란 주어진 테스트가 얼마나 많은 코드를 검증하느냐를 말합니다.

좁은 범위 테스트(narrow-scoped test, 보통 단위 테스트 라고 함)는 독립된 클래스나 메서드같이 코드베이스 중 작은 일부 로직을 검증하도록 설계됩니다.

중간 범위 테스트(medium-scoped test, 보통 통합 테스트 라고 함)는 적은 수의 컴포넌트들 사이의 상호작용을 검증하도록 설계됩니다. 가령 서버와 데이터베이스의 상호작용을 검증합니다.

넓은 범위 테스트(large-scoped test, 보통 기능 테스트, 종단간 테스트, 시스템 테스트 등으로 불림)는 시스템의 서로 다른 부분들 사이의 상호작용, 혹은 클래스나 메서드 하나만 실행할 때는 괜찮다가 여럿을 조합해 실행하면 나타나는 예기치 못한 동작을 검증하도록 설계됩니다.

단위 테스트의 범위가 좁다고 할 때는 ‘실행’되는 코드가 아니라 ‘검증’되는 코드의 양이 기준입니다. 하나의 클래스가 다른 여러 클래스를 의존하거나 참조하는 경우는 흔하며, 대상 클래스를 테스트하는 과정에서 자연스럽게 의존하는 코드도 호출하게 됩니다.

일부 다른 테스트 전략 (마틴 파울러 - mocksArentStubs 글의 Classical and Mockist Testing 섹션 참고)에서는 가짜 객체(fake object)나 모의 객체(mock object) 같은 테스트 대역을 많이 활용하여 테스트 대상 시스템 바깥 코드가 실행되는 일을 피하기도 합니다. 하지만 구글은 딱히 실행할 수 없는 상황이 아니라면 실제 의존성을 끊지 않는 편을 선호합니다.

좁은 범위 테스트는 작은 테스트가 되고 더 넓은 범위 테스트들은 중간 크기나 큰 테스트가 되는 것이 일반적입니다. 하지만 꼭 그렇지는 않습니다.

예를 들면 데이터베이스와 파일시스템 같은 프로세스 외부 의존성을 모두 테스트 대역으로 대체한다고 했을 때, 파싱, 요청 유효성 검사, 비즈니스 로직을 모두 포함하는 서버의 테스트도 작을 수 있습니다.

비슷하게, 메서드 하나를 검사하는 좁은 범위 테스트라도 중간 크기여야만 할 수 있습니다. 예를 들어 최신 웹 프레임워크들은 HTML과 자바스크립트를 묶어 배포되는 경우가 많습니다. 그래서 날짜 선택기 같은 UI 요소의 경우 실행 경로 하나만 테스트하려 해도 반드시 스라우저까지 실행해야 합니다.

구글은 되도록 작은 테스트를 추구하며, 좁은 범위 테스트를 추구합니다.

google-test-pyramid

구글은 비즈니스 로직 대부분을 검증하는 좁은 범위의 단위 테스트가 80% 이상, 둘 이상의 구성요소 간 상호작용을 검증하는 중간 범위의 통합 테스트가 15%, 전체 시스템을 검증하는 종단간 테스트가 5% 정도가 되도록 합니다.

단위 테스트는 빠르고 안정적이며 범위를 극적으로 좁혀줘서 클래스나 함수가 제공하는 동작들은 적은 노력으로도 모두 식별하게 해줍니다. 기초를 튼튼하게 다져줍니다. 또한 실패 시 원인을 빠르게 진단할 수 있습니다.

하지만 주의 해야할 안티 패턴이 있습니다. 바로 아래의 ‘아이스크림 콘’과 ‘모래 시계’ 입니다.

anti-pattern-test-pyramid

아이스크림 콘에서는 엔지니어들이 종단간 테스트를 많이 작성하고 통합 테스트나 단위 테스트는 훨씬 적게 작성합니다. 이런 테스트 스위트는 일반적으로 느리고 신뢰할 수 없으며 고치기도 어렵습니다. 프로토타입에서 시작하여 급하게 프로덕션으로 이전하느라 테스트 부채를 미처 해결하지 못한 프로젝트에서 자주 나타나는 안티 패턴입니다.

모래시계는 종단간 테스트와 단위 테스트는 많지만 통합 테스트가 적습니다. 모래시계는 아이스크림 콘만큼 나쁘지는 않습니다. 하지만 중간 범위 테스트였다면 더 빠르고 쉽게 해결했을 문제들을 종단간 테스트로 막아내고 있습니다. 모래시계 패턴은 구성요소들이 강하게 커플링되어 각각의 인스턴스를 독립적으로 만들어낼 수 없을 때 나타납니다.

구글이 추천하는 테스트 구성 비율은 엔지니어링 생산성과 제품 신뢰도라는 두 가지 핵심 목표를 고려해 정한 것입니다. 풍부한 단위 테스트는 개발 초기에 빠르게 신뢰도를 높여줍니다. 더 큰 테스트들의 역할은 제품이 제 모습을 갖춰감에 따른 온전성 검사입니다. (버그를 잡기 위한 주된 수단으로 바라봐서는 안 됩니다.)

통합 테스트 비율을 늘리면 테스트 스위트의 전체 수행 시간은 길어지지만 구성요소 사이의 문제를 더 많이 잡아낼 수 있을 것입니다. 단위 테스트 비율을 늘리면 테스트 스위트가 매우 빠르게 완료되고 일반적인 로직 버그를 많이 잡아낼 것입니다. 하지만 단위 테스트는 구성요소 간 상호작용은 검중할 수 없습니다. 따라서 다양한 크기와 범위의 테스트가 시스템 아키텍처와 조직의 현실에 맞게끔 조화롭게 혼합되어 있다면 좋은 테스트 스위트라고 할 수 있습니다.

11.2.3 비욘세 규칙

새로 합류한 직원들을 지도하다 보면 어떤 행위나 속성을 테스트해야 하냐는 질문을 자주 받습니다. 간단한 대답은 ‘깨뜨려보고 싶은 모든 것을 테스트하라’ 입니다.

시스템이 특정 행위를 올바르게 수행하는지 확신하고 싶다면 그 행위를 검증하는 자동 테스트를 작성하는 것만이 유일한 선택지입니다. 여기에는 성능, 행위 정확성, 접근성, 보안처럼 의심해볼 수 있는 모든 것이 다 포함됩니다.

이 일반적인 철학에 ‘비욘세 규칙’ 이라는 이름을 붙였습니다. ‘네가 좋아 했다면 (CI) 테스트를 준비해뒀어야지’라는 뜻입니다.

의도적으로 실패 상황을 만드는 테스트 실패는 시스템이 고려해야 하는 가장 중요한 상황 중 하나입니다. 하지만 시스템이 재난에 잘 대응하는지를 보기 위해 실제로 재난이 일어날 때까지 기다리는 것은 어리석습니다. 실패할 때까지 기다리는 대신 흔한 유형의 실패 상황을 시뮬레이션하는 자동화 테스트를 작성하세요.
단위 테스트에서라면 예외나 에러를 시뮬레이션해볼 수 있고, 통합 테스트나 종단간 테스트에서라면 원격 프로시저 호출(RPC) 옹류를 주입하거나 지연시간을 늘려볼 수 있습니다.
카오스 엔지니어링(chaos engineering) 같은 기법을 활용하면 실제 프로덕션 네트워크에 영향을 주는 훨씬 더 큰 중단 사태도 시뮬레이션할 수 있습니다.
신뢰할 수 있는 시스템이라면 부정적인 조건을 예측하고 대응 방식을 통제할 수 있어야 합니다.

11.2.4 코드 커버리지

코드 커버리지는 어느 테스트가 기능 코드의 어느 라인을 실행하는지를 측정하는 수단입니다. 100라인짜리 코드가 있고 테스트가 90라인을 실행했다면 코드 커버리지는 90%입니다.

코드 커버리지는 테스트 품질을 파악하는 표준 지표로 간주되기도 하는데, 조금 안타까운 일입니다. 적은 수의 테스트만으로 상당량의 라인을 실행하면서도 의미 있는 동작은 거의 돌려보지 않을 수 있기 때문입니다. 또한 코드 커버리지는 호출된 라인 수만 셀 뿐, 실행 결과로 어떤 일이 벌어졌는지는 고려하지 않기 때문입니다. (큰 테스트는 커버리지 인플레이션을 일으키므로 커버리지는 작은 테스트에서만 측정하길 권합니다.) 더 큰 문제는 커버리지 자체가 목표가 되기 쉽습니다.

테스트 스위트의 품질을 높이는 더 나은 방안은 무엇일까요? 바로 검사해야 할 행위에 집중하는 것입니다. 고객이 이용할 모든 기능이 제대로 동작한다고 확신하나요? 의존하는 외부 시스템이나 모듈에 파괴적인 변경(breaking change)이 일어났을 때 (즉, 호환성이 깨졌을 때) 바로 인지하고 대응할 수 있을까요? 작성해둔 테스트는 안정적이고 믿을만한가요? 이런 질문들이 테스트 스위트의 전체적인 모습을 더 잘 대변해줍니다.

단 하나의 수치로 ‘테스트가 충분히 갖춰졌냐?’ 라는 질문에 답하려 들면 수많은 맥락을 놓치게 되고 유용하지도 않습니다. 코드 커버리지는 테스트되지 않은 코드가 어디인지는 알려줄 수 있지만, 시스템이 얼마나 제대로 테스트되었느냐를 판가름하는 지표로는 적합하지 않습니다.