Skip to content

Latest commit

 

History

History
495 lines (378 loc) · 53.2 KB

14. Larger Testing.md

File metadata and controls

495 lines (378 loc) · 53.2 KB

14. 더 큰 테스트

  • 구글에서는 중간 크기와 큰 테스트처럼 더 큰 테스트를 많이 활용하고 있다. 더 큰 테스트들도 건실한 소프트웨어 엔지니어링에 필요한 위험 완화 전략에서 중요한 역할을 한다.
  • 세월이 흘러도 효과를 유지하게끔 해주는 모범 사례

14.1 더 큰 테스트란?

  • 테스트의 크기(size)
    • 작은 크기 테스트: 단일 스레드 또는 단일 프로세스
    • 중간 크기 테스트: 단일 기기
  • 테스트의 범위(scope)
    • 단위 테스트
    • 통합 테스트
    • 종단간 테스트: 의존하는 외부 모듈을 직접 이용하며 테스트 대역은 거의 쓰지 않는다.
  • 더 큰 테스트의 특성
    • 느릴 수 있다. 구글에서 대규모 테스트의 기본 타임아웃 값은 15분이나 1시간이다. 심지어 몇 시간이나 며칠이 걸리는 테스트도 만들어 활용한다.
    • 밀폐되지 않을 수 있다. 대규모 테스트는 다른 테스트나 최종 사용자와 자원 및 트래픽을 공유하기도 한다.
    • 비결정적일 수 있다. 예컨대 밀폐되지 않은 대규모 테스트라면 다른 테스트나 사용자 상태에 영향을 받을 수 있어서 완벽히 결정적이라고 보장하기가 거의 불가능하다.
  • 단위 테스트는 개별 함수, 객체, 모듈에 대한 확신을 심어준다. 반면 더 큰 테스트들은 시스템 전체가 의도대로 동작한다는 확신을 더해주는 역할을 한다. 또한 이들을 자동화해두면 다양하게 확장할 수 있다.

14.1.1 충실성

  • 더 큰 테스트가 존재하는 첫 번째 이유는 충실성을 높이기 위함이다.
  • 충실성(fidelity): 테스트가 대상의 실제 행위를 얼마나 충실하게 반영했느냐를 나타내는 속성
    • 테스트 크기와 충실성: 단위테스트 < 단일 프로세스 SUT < 격리된 SUT < 스테이징 < 프로덕션
  • 단위 테스트가 대상 코드를 검증해주는 건 분명하지만 프로덕션 환경에서는 다르게 동작할 것이다.
  • 프로덕션은 테스트 충실성이 가장 높은 환경이다.
  • 더 큰 테스트의 핵심은 이 사이에서 가장 적합한 지점을 찾아내는 것이다. 충실성이 높아질수록 비용이 커져서 테스트 실패 시 입는 손해도 크기 때문이다.
  • 제품을 런칭하기 전에는 현실적인 테스트 트래픽을 만들어내기 쉽지 않다. 단위 테스트용 데이터는 대부분 수작업으로 만들어진다. 그래서 다루는 사례가 몇 안 되고 제작자의 편견이 반영되기 쉽다. 이처럼 데이터에서 누락되어 다루지 못한 시나리오가 테스트의 충실성 하락으로 이어진다.

14.1.2 단위 테스트가 손 대기 어려운 영역

1) 부정확한 테스트 대역

  • 보통 단위 테스트는 클래스나 모듈 하나를 테스트한다.
  • 무겁고 테스트하기 어려운 의존성을 제거하는 용도로 테스트 대역을 많이 쓴다. 하지만 이렇게 하면 실제와 대역의 동작이 일치하지 않을 가능성이 생긴다.
    • 엔지니어가 의존 대상의 실제 동작을 잘못 이해하거나 약속을 오해하는 경우
    • 모의객체는 부실하여 실제 구현이 수정될 때 테스트와 테스트 대상 코드도 함께 수정되어야 한다는 신호를 주지 못한다. 그러므로 각 팀에서 담당 서비스의 가짜 객체를 제공하여 이러한 우려를 제거하는 것이 바람직하다.

2) 설정 문제

  • 단위 테스트는 주어진 바이너리 내의 코드를 다룬다. 하지만 일반적으로 이 바이너리는 단독으로 실행될 수 없다 (배포 설정, 시작 스크립트 등 필요).
  • 설정 파일에 문제가 있거나, 데이터베이스에 정의된 상태와 다르게 테스트한 후 프로덕션에 배포하면 사용자에게 심각한 문제를 일으킬 수 있다. 단위 테스트만으로는 이러한 호환성 문제를 검증할 수 없다. 그러므로 설정도 코드처럼 버전 관리를 해야 한다.
  • 설정도 코드처럼 버전 관리를 하면 버그의 원인으로 관리할 수 있어서 통제 안 되던 외부 위험에서 벗어날 수 있고 더 큰 테스트에 포함시킬 수도 있다.

3) 과부하 시 나타나는 문제

  • 단위 테스트는 작고 빨라야 한다. 표준 테스트 실행 인프라에 적합해야 하고 개발자 워크플로에 매끄럽게 융화되어 자주 실행되어야 하기 때문이다.
  • 성능, 부하, 스트레스 트스트는 바이너리에 상당한 양의 트래픽을 일으키므로 통상적인 단위 테스트 모델에 녹이기 어렵다 (상당한 양: 초당 수천에서 수백만 번의 쿼리).

4) 예기치 못한 동작, 입력, 부작용

  • 단위 테스트의 범위는 작성자의 상상력에 갇히게 된다. 작성자가 예상할 수 잇는 행위와 입력에 한해서 테스트되기 쉽다.
  • 사용자들은 엔지니어가 예상하지 못한 문제를 찾아내는 경우가 아주 많다. 이는 예상치 못한 행위를 테스트하는 다른 기술이 필요하다는 방증이다.
  • 하이럼의 법칙. 우리가 약속된 모든 기능을 100% 테스트하더라도, 실제 이용자들은 명시된 약속뿐 아니라 눈에 보이는 모든 것을 자유롭게 이용해볼 수 있다.

5) 창발적 행위와 진공효과 (창발적: 전혀 예기치 않은 새로운 행동 양식이 갑자기 나타남)

  • 단위 테스트가 다루는 범위는 제한적이며, 이 범위 밖의 행위가 바뀌는 건 알아챌 수 없다.
  • 단위 테스트는 빠르고 안정적이게끔 설계하기 때문에 실제로 의존하는 바이너리 혹은 현실 세계의 네트워크와 데이터에 연결했을 때 발생할 수 있는 혼돈은 의도적으로 배제한다.
  • 그러므로 속도와 안정성은 매우 뛰어나지만 특정 범주의 결함들은 놓치기 쉽다.

14.1.3 더 큰 테스트를 만들지 않는 이유

  • 좋은 단위 테스트라면 아래의 특징을 모두 갖춰야 한다.
    • 높은 신뢰성: 결과가 불규칙하면 안 되며 유용한 성공/실패 신호를 제공해야 한다.
    • 빠른 속도: 개발자 워크플로를 방해하지 않을 정도로 빨라야 한다.
    • 높은 확장성: 구글은 변경되는 코드에 영향을 받는 모든 테스트를 서브밋 직전과 직후에 효율적으로 실행할 수 있어야 한다.
  • 더 큰 테스트에서는 위와 같은 특징을 하나도 갖추지 못하는 경우도 생긴다 (더 많은 인프라 사용으로 인해 비결정적인 결과가 초래되고 더 오래 걸린다).
  • 더 큰 테스트가 극복해야 할 과제
    • 소유권 문제: 단위 테스트는 누가 소유자인지 명확하다. 더 큰 테스트는 다수의 단위에 걸쳐 있으므로 관련된 소유자 역시 많다. 시간이 흐를수록 소유권이 더 모호해진다. 유지보수는 누가 책임지고, 테스트가 실패하면 누가 문제를 진단해야 하는가? 소유권이 명확하지 않다면 테스트는 서서히 부패할 것이다.
    • 표준화 혹은 표준화 부족: 단위 테스트와 달리 더 큰 테스트는 작성하고 실행하고 디버깅하기 위한 인프라와 프로세스가 부실하다. 더 큰 테스트를 수행하는 방식은 시스템 아키텍처에 따라 달라지므로 테스트 유형이 다양하다. 모든 테스트를 수행하는 표준화된 방식이 없으므로 자연스럽게 인프라의 지원을 받지 못한다. 대규모 변경을 진행하는 사람들이 연관된 팀들의 테스트 모두 수행 방법을 파악하도록 하는 방식은 확장성이 떨어진다. 더 큰 테스트는 팀마다 구현 방식이 다르기 때문에 이 팀들의 제품을 통합해 테스트하려면 먼저 호환되지 않는 인프라들부터 통합해야 한다.

14.2 더 큰 테스트 @구글

  • 구글은 2003년 이전에도 자동화 테스트를 사용했는데, 일반적으로 종단간 테스트와 같이 진짜 큰 규모로 테스트를 수행했다.
  • 단위 테스트와 그 외 테스트를 구분하게 된 중요한 진전
    • 테스트 피라미드를 장려했다. 대다수 테스트가 단위 테스트가 되길 원하여 단위 테스트에 집중했다.
    • 훗날 C/J Build를 대신하여 TAP을 공식 지속적 빌드 시스템으로 도입하였다.

14.2.1 더 큰 테스트와 수명

  • 더 큰 테스트들은 시간이라는 관점에서 어떤 영향을 주는가?
  • 어떤 활동은 코드 기대 수명이 길수록 더 가치를 발하며, 테스트란 기대 수명에 상관없이 의미가 있는 활동이다. 하지만 어떤 테스트가 가장 적합한지는 코드의 기대 수명에 따라 달라진다.
  • 단위 테스트는 기대 수명이 몇 시간 이상만 되면 충분히 가치가 있다.
  • 작은 스크립트처럼 수명이 몇 분 수준이라면 수동 테스트가 가장 일반적이며, 이 때 테스트 대상은 보통 로컬에서 실행된다.
  • 더 큰 테스트들 모두 수명이 더 긴 소프트웨어에 유용하다. 하지만 수명이 길어질수록 주 관심사가 테스트의 유지보수로 옮겨간다. 시간이 주는 이 영향 때문에 '아이스크림 콘'이라는 안티패턴이 생겨났다.
  • 개발 초기에 코드가 몇 분 정도만 쓰이고 사라질 거라 판단하여 수동 테스트에 의존하면 수동 테스트들이 누적되어 초기 테스트 포트폴리오 전체를 지배하게 된다.
    • 처음에 구현한 방식 때문에 코드가 단위 테스트를 하기 어렵게 짜여 있다면 자동화할 수 있는 테스트는 오직 종단간 테스트뿐이다.
  • 건강한 상태를 오래 유지하는 핵심은 개발 시작 후 며칠 안으로 단위 테스트를 만들어 테스트 피라미드를 쌓기 시작하는 것이다. 그런 다음 수동으로 수행하던 종단간 테스트를 자동화된 통합 테스트로 대체해 피라미드 위층으로 올린다. 구글은 코드를 서브밋하려면 반드시 단위 테스트를 포함하도록 규정하여 해결했다.
  • 오랫동안 코드를 건강하게 유지하려면 단위 테스트와 수동 테스트 사이의 간극을 메우는 데 소홀해서는 안 된다.

14.2.2 구글 규모에서의 더 큰 테스트

  • 규모가 큰 소프트웨어라면 더 큰 테스트가 그만큼 더 필요하고 유용하다. 하지만 작성하고 수행하고 관리하고 디버깅하는 복잡도는 규모가 커질수록 함께 증가한다.
  • 종단간 테스트가 필요한 개별 시나리오의 수는 SUT의 구조에 따라 기하급수적으로 혹은 조합의 수만큼 늘어나기 마련이므로 이런 식의 성장은 확장하는 데 한계가 분명하다. 따라서 시스템의 성장에 맞춰 더 큰 테스트들도 지속해서 관리할 수 있으려면 새로운 전략을 모색해야 한다.
  • 서비스 규모를 이만큼 키우기 위해 구성요소의 수를 늘려놨기 때문에 더 큰 테스트들을 통해 얻는 가치 역시 커진다. 그리고 이 가치에는 충실성이 큰 영향을 준다.
  • 충실성이 낮은 테스트 대역을 사용하면 버그가 생길 가능성이 매우 높으므로 더 큰 테스트들은 원하는 규모에서 잘 작동하면서도 충실성이 상당히 높게 구현하는 게 관건이다.

TIP 가능한 한 작은 테스트

  • 통합 테스트라 하더라도 가능한 한 작을수록 좋다. 초거대 테스트 하나보다 거대한 테스트 여러 개가 낫다는 말이다.
  • 테스트 범위는 SUT의 범위와 관련이 높기 때문에 SUT를 더 작게 만드는 방법을 찾으면 테스트를 더 작게 만드는 데 유리하다.
  • 여러 개의 내부 시스템이 관여하는 기능을 테스트하면서 테스트 크기를 작게 만드는 좋은 전략으로 '연쇄 테스트(chain test)'라는 방법이 있다. 기능 전체를 아우르기보다는 작은 통합 테스트들로 나눠 연결한다. 이 때 앞 단계의 테스트 결과를 리포지터리에 저장한 다음, 그다음 단계 테스트의 입력으로 사용한다.

14.3 큰 테스트의 구조

  • 큰 테스트들은 작은 테스트의 제약조건에 구속받지 않아서 어떤 형태로든 만들 수 있지만, 그래도 대부분은 공통된 패턴을 따른다.
  • 큰 테스트를 진행하는 일반적 흐름
    1. 테스트 대상 시스템 확보
    2. 필요한 테스트 데이터 준비
    3. 대상 시스템을 이용해 동작 수행
    4. 행위 검증

14.3.1 테스트 대상 시스템

  • 대규모 테스트의 핵심은 테스트 대상 시스템(SUT)이다.
  • 대규모 테스트에서의 SUT는 대체로 사정이 많이 달라서 하나 이상의 독립된 프로세스에서 수행된다.
  • SUT의 형태는 주로 다음 두 요소에 의해 결정된다.
    • 밀폐성: SUT는 현재 테스트하려는 기능과 관련 없는 구성요소를 사용하거나 상호작용하지 못해야 한다. 밀폐성이 높은 SUT는 동시성 문제나 불규칙한 인프라로부터 영향을 적게 받는다.
    • 충실성: SUT는 테스트 중인 프로덕션 시스템을 충실히 반영해야 한다. 충실설이 높은 SUT는 프로덕션 버전과 유사한 바이너리로 구성된다(비슷한 설정, 인프라, 토폴로지).
  • 위 두 요소가 충돌할 때가 많은데, 다음 SUT 형태들에서 어떻게 충돌하는지 확인한다.
    • 단일 프로세스 SUT (프로덕션 시스템에서는 여러 독립 프로세스로 구동되더라도) SUT 전체가 하나의 바이너리로 패키징되고, 나아가 테스트 코드까지 함께 패키징된다. 이러한 테스트-SUT 조합 형태에서 모든 것이 단일 스레드로 실행된다면 '작은' 테스트가 될 수 있다. 하지만 충실성 측면에서는 프로덕션의 토폴로지나 설정과 거리가 가장 먼 테스트가 된다.
    • 단일 머신 SUT SUT는 (프로덕션과 똑같이) 하나 이상의 독립 바이너리로 구성되며, 테스트도 별도의 바이너리로 만들어진다. 하지만 모두가 하나의 머신에서 구동한다. 구글에서 '중간 크기' 테스트라고 하는 형태이다. 이상적으로는 SUT 바이너리를 비록 로컬에서 실행하더라도 충실성을 높이기 위해 프로덕션 실행 설정을 그대로 사용한다.
    • 다중 머신 SUT (클라우드에 배포된 프로덕션과 비슷하게) SUT를 여러 머신에 분산시킨다. 단일 머신 SUT보다 충실성이 높지만, 테스트 규모가 커지면서 여러 머신과 그 사이를 잇는 네트워크가 불안정성을 키워 테스트에 예기치 못한 영향을 줄 가능성이 커진다.
    • 공유 환경(스테이징과 프로덕션) SUT를 독립적으로 실행하는 대신 테스트에서 공유 환경을 직접 사용한다. 이미 운용 중인 공유 환경에 얹히는 것이라서 추가 비용이 가장 적게 든다. 하지만 공유 환경을 함께 이용 중인 다른 엔지니어들과 충돌할 수 있으므로 테스트만을 위해 임의로 변경할 수 없고 정상적인 배포 워크플로를 따라 코드가 해당 환경에 배포될 때까지 기다려야 한다. 또한 프로덕션 환경일 경우 최종 사용자에게 영향을 줄 위험이 있다.
    • 하이브리드 어떤 SUT는 혼합된 형태를 띤다. SUT의 구성요소 중 일부는 독립적으로 실행하고, 다른 일부는 공유 환경에서 가동 중인 서비스와 상호작용하는 식이다. 예컨대 테스트할 대상 기능은 직접 실행하지만 백엔드가 공유되고 있을 수 있다. 구글처럼 빠르게 확장되는 회사는 상호 연결된 서비스가 매우 많아서 모든 서비스 각가을 여러 벌 복사해서 실행하는 것이 사실상 불가능하다. 그래서 어느 정도의 하이브리드화는 피할 수 없다.

밀폐된 SUT의 이점

  • 큰 테스트에서 SUT는 테스트 신뢰성을 떨어뜨리고 피드백 시간을 늘리는 주범이 될 수 있다.
    • 프로덕션 환경에서의 테스트는 실제 운영 중인 시스템을 이용한다. 테스트 코드가 프로덕션 환경에 배포될 때까지 대기해야 한다. 환경에 릴리즈되는 시점을 테스트 수행자가 직접 통제하지 못한다(SUT가 너무 늦게 준비된다).
    • 흔한 대안으로 거대한 공유 스테이징 환경을 만들고 테스트를 그 안에서 실행하는 방법이 있다. 테스트용 코드가 공유 환경에 반영된 후에야 테스트를 할 수 있다는 한계가 여전하다.
    • 다른 안으로 엔지니어가 스테이징 환경에서 사용할 수 있는 시간을 예약해두고 그 시간 동안은 계류 중인 코드를 배포하고 테스트를 실행해볼 수 있도록 하는 방법도 있다. 환경 자체, 사용자 수, 사용자들 사이의 충돌 가능성이 급속도로 커지기 때문에 엔지니어 수나 서비스 수가 늘어나면 지속하기 어려운 방법이다.
    • 클라우드에서 격리된 영역을 만들거나 머신을 밀폐할 수 있는 환경을 구축하고 그 안에 SUT를 배포하는 방법도 있다. 이러한 환경이 갖춰지면 충돌 걱정이나 시간 예약 없이 코드를 릴리즈할 수 있다.
  • 테스트 데이터를 최종 사용자가 발견할 가능성을 항상 염두에 두고 대비해야 한다.

문제 경계에서 SUT 크기 줄이기

  • 테스트를 하다 보면 웬만해서는 피해야 할 고통스러운 경계가 존재한다. 프론트엔드와 백엔드가 만나는 경계가 대표적이다. 이 경계를 포괄하는 테스트는 되도록 피해야 한다. UI 테스트는 신뢰하기 어렵고 비용도 많이 들기 때문이다.
    • UI는 look-and-feel 차원에서 달라지는 경우가 잦아서, 실제 동작은 완전해도 UI 테스트를 깨지기 쉽게 만든다.
    • UI는 주로 비동기 방식으로 반응하기 때문에 테스트하기 어렵다.
  • 서비스 UI를 백엔드까지 포함시켜 종단간으로 테스트해보는 것도 의미가 있지만, 이런 테스트는 유지보수 비용이 너무 크다.
    • 만약 백엔드가 공개 API를 제공한다면 테스트를 UI/API 경계에서 나누고, 종단간 테스트는 공개 API를 이용해 수행하는 편이 훨씬 쉽다. UI가 웹 브라우저든 CLI든 데스크톱 혹은 모바일 앱이든 모두 마찬가지이다.
  • 또 다른 경계로 서드파티 의존성을 들 수 있다. 서드파티 시스템은 대체로 테스트를 위한 공유 환경을 따로 제공하지 않고, 서드파티로 보내는 트래픽에 비용이 매겨지는 경우도 있으므로 실제 서드파티 API를 직접 사용하는 자동 테스트는 권장하지 않는다.

기록/재생 프록시

  • 모의 객체와 스텁, 똑같은 API를 제공하는 가짜 서버와 프로세스를 이용하면 서버나 프로세스 전체를 대신하는 대역도 만들 수 있다. 그러나 이때 테스트 대역이 원래의 대상과 완전히 똑같이 동작한다는 보장은 없다.
  • SUT가 의존하지만 보조적인 서비스라면 테스트 대역으로 대체할 수 있다. 하지만 이 대역이 원래 의존 대상의 실제 동작을 그대로 반영하는지 알 수 있는지는 어떻게 알 수 있는가?
    • 구글 외부에서는 고객 주도 계약(consumer-driven contract) 테스트용 프레임워크를 활용하는 사례가 늘고 있다. 고객과 서비스 제공자 모두가 지켜야 할 계약(명세)을 정의하고, 이 계약을 토대로 자동화된 테스트를 만들어내는 방식이다. 고객이 서비스의 모의 객체를 정의하며 이때 어떤 입력을 주면 어떤 결과를 받게 되는지를 명시한다. 그런 다음 실제 서비스는 이 입력/결과쌍을 실제 테스트에 활용하여 기대한 결과를 반환하는지를 검증한다.
    • 고객 주도 계약 테스트용 도구로는 Pact Contract Testing과 Spring Cloud Contracts가 유명하다. 하지만 구글은 프로토콜 버퍼를 광범위하게 쓰고 있어서 이 두 도구를 활용하지 않는다.
  • 구글은 공개 API가 있다면 더 큰 테스트를 실행해 외부 서비스들과의 트래픽을 기록해뒀다가, 이 트래픽을 더 작은 테스트를 수행할 때 재생하는 방법을 사용한다. 더 큰 테스트 혹은 기록 모드 테스트를 포스트서브밋 테스트로 항시 수행하여 트래픽 로그를 생성하고(테스트 통과 시에만 로그 생성), 더 작은 테스트 혹은 재생 모드 테스트를 개발 시 혹은 프리서브밋 테스트로 활용한다.
    • 기록/재생 방식에서 흥미로운 점은 비결정성을 없애기 위해 매칭기(matcher)를 이용하여 요청을 보고 기대하는 응답과 연결시킨다는 것이다. 스텁이나 모의 객체를 사용할 때 인수를 보고 결과 행위를 결정하는 방식과 매우 비슷하다.
    • 새로운 테스트를 추가할 때 혹은 클라이언트의 행위가 크게 달라지면 기록해둔 트래픽과 요청이 더 이상 일치하지 않을 것이므로 재생 모드 테스트가 실패한다. 이 경우 기록 모드 테스트를 엔지니어가 따로 수행하여 새로운 트래픽을 생성해야 한다. 따라서 기록 모드 테스트를 수행하기 쉽고 빠르게 안정되게 만드는 일 역시 중요하다.

14.3.2 테스트 데이터

  • 테스트에는 데이터가 필요하고, 대규모 테스트라면 두 가지 데이터가 필요하다.

    • 시드 데이터 테스트 개시 시점의 SUT 상태를 반영하여 SUT를 사전 초기화해주는 데이터
    • 테스트 트래픽 테스트 수행 과정에서 SUT로 보내는 데이터
  • SUT의 상태를 테스트 전에 초기화해두는 작업 예시

    • 도메인 데이터 어떤 데이터베이스는 환경 구성용 데이터가 테이블들에 미리 채워져 있어야 한다. 실제 서비스에서 이런 데이터베이스를 이용한다면 적절한 도메인 데이터 없이는 테스트를 제대로 시작할 수 없다.
    • 현실적인 기준선 현실적인 SUT가 되려면 품질과 양적 측면 모두에서 현실적인 데이터셋이 기본으로 갖춰져 있어야 할 것이다. 예컨대 소셜 미디어의 거대 테스트라면 사전에 현실적인 소셜 그래프가 구축되어 있어야 한다. 다시 말해, 현실적인 프로필을 갖춘 사용자가 충분히 많아야 하고, 동시에 사용자들 사이의 관계 역시 충분히 맺어져 있어야 의미 잇는 테스트를 진행할 수 있다.
    • 데이터 기록 API 데이터를 기록하는 API가 복잡할 수 있다. 테스트에서 데이터 리포지터리에 직접 쓸 수도 있지만, 이렇게 하면 실제 바이너리가 데이터를 기록할 때 수행하는 트리거나 검사 로직이 수행되지 않는다.
  • 데이터 생성 방법

    • 손수 가공한 데이터 작은 테스트에서처럼 더 큰 테스트용 데이터도 사람이 손수 만들 수 있다. 예컨대 실제 운영 중인 지도 서비스의 데이터를 복사하여 기준으로 삼아 변경 사항을 테스트할 수 있다.
    • 샘플링한 데이터 운영 중인 서비스에서 복사한 데이터는 너무 거대하여 테스트 목적으로는 적합하지 않을 수 있다. 대신 표본을 추출해 사용하면 테스트 시간이 단축되고 분석하기도 쉬워진다. 테스트 커버리지를 극대화할 수 잇는 최소한의 데이터만 추출해내는 기술을 '스마트 샘플링(smart sampling)'이라 한다.

    14.3.3 검증

    • SUT가 구동되고 트래픽이 보내졌다면 제대로 작동했는지 검증해야 한다. 검증 방식도 여러가지가 있다.
      • 수동 검증 사람이 SUT와 직접 상호작용하며 올바르게 동작하는지 확인하는 걸 말한다. 바이너리를 로컬에서 사용해볼 때와 마찬가지다. 이 방식은 테스트 계획에 정의된 조치대로 일관되게 회귀 테스트를 수행하거나, 혹은 색다른 상호작용 시나리오를 찾아 잠재된 새로운 결함을 찾는 탐색적 테스팅 용도로 쓰인다. 수동 회귀 테스트는 선형으로 확장되지 않는다는 점을 기억해야 한다. 시스템이 커지면 확인해야 할 시나리오는 그보다 훨씬 빠르게 증가하므로 더 많은 인력과 시간을 투입해야 한다.
      • 단정문 단위 테스트처럼, 시스템이 의도된 대로 동작하는지 명확히 검사하는 검증 방식이다. 예를 들어 구글에서 'xyzzy'를 검색하는 통합 테스트라면 다음과 같은 단정문을 수행한다.
        assertThat(response.Contains("Colossal Cave"))
      • A/B 비교 A/B 테스트는 두 벌의 SUT를 구동시켜 똑같은 데이터를 보낸 다음 결과를 비교하는 검증 방식이다. 일반적으로 의도된 행위가 사전에 명확하게 정의되어 있지 않아서 사람이 직접 차이를 살펴 의도된 변경인지를 확인해야 한다.

14.4 더 큰 테스트 유형

  • 구글에서 사용하는 큰 테스트 종류
    • 하나 이상의 바이너리에 대한 기능 테스트
    • 브라우저와 기기 테스트
    • 성능, 부하, 스트레스 테스트
    • 배포 설정 테스트
    • 탐색적 테스팅
    • A/B 차이(회귀) 테스트
    • 사용자 인수 테스트(UAT)
    • 프로버와 카나리 분석
    • 재해 복구와 카오스 엔지니어링
    • 사용자 평가
  • 테스트 종류가 많은 만큼 가능한 조합도 많다. 그렇다면 어떤 테스트를 언제 수행하는가?
  • 구글은 소프트웨어 설계의 일환으로 테스트 계획의 초안도 작성한다. 그리고 이 계획의 핵심은 어떤 테스트가 필요하고 각각의 테스트를 얼마나 많이 수행해야 하는지에 관한 개략적인 전략 확립이다.
  • 테스트 전략은 주요한 위험 요소를 찾고, 찾아낸 위험 요소들을 완화해주는 테스트 방식을 정하는 것이다.
  • 구글은 테스트 엔지니어라는 역할이 있고, 이 엔지니어에게 기대하는 역할이 바로 우리 제품이 적합한 테스트 전략의 윤곽 그리기이다.

14.4.1 하나 혹은 상호작용하는 둘 이상의 바이너리 기능 테스트

  • 테스트 특성
    • SUT: 밀폐된 단일 머신 혹은 격리된 클라우드에 배포
    • 데이터: 수동 생성
    • 검증 방식: 단정문
  • 단위 테스트는 실제 코드와는 다르게 패키징된다는 이유만으로도 복잡한 시스템을 충실히 검증하기에는 적합하지 못하다. 대다수 기능 테스트 시나리오가 대상 바이너리와 상호작용하는 방식은 클래스들 사이에서 이루어지는 상호작용과는 다르다. 따라서 기능 테스트는 독립된 SUT를 대상으로 수행되어야 한다.
  • 서비스 각각을 독립된 바이너리로 배포하는 마이크로서비스 환경의 경우 기능 테스트라면 관련 바이너리를 모두 포함한 SUT를 구동시키고 오픈 API를 통해 바이너리 사이의 실제 상호작용을 검증할 수 있다.

14.4.2 브라우저와 기기 테스트

  • 웹 UI나 모바일 애플리케이션 테스트 역시 하나 이상의 바이너리와 상호작용하는 기능 테스트의 한 형태이다.
  • 기반 코드는 단위 테스트로 검증할 수 있지만, 최종 사용자에게는 공개 API가 애플리케이션 자체일 수 있다.
  • 서드파티 입장이 되어 프론트엔드를 통해 애플리케이션을 이용하는 테스트는 커버리지를 높여주는 또 다른 수단이 되어준다.

14.4.3 성능, 부하 스트레스 테스트

  • 테스트 특성
    • SUT: 격리된 클라우드에 배포
    • 데이터: 수동 생성 혹은 프로덕션 환경에서 복사
    • 검증 방식: 차이 비교(성능 지표)
  • 성능, 부하, 스트레스 테스트를 작은 단위로 진행할 수도 있찌만, 때로는 외부 API르 써서 동시다발적인 트래픽을 감당할 수 있는지 확인해야 한다.
  • 정의상 주로 바이너리 검증용에 쓰이는 다중 스레드 테스트라 할 수 있다.
  • 버전업 시 성능 저하는 없는지, 그래서 시스템이 목표한 최대 트래픽을 감당할 수 있는지를 확인하는 데 필수적인 테스트이다.
  • 배치 토폴로지를 수정하여 성능 테스트에서의 노이즈를 제거하려는 연구가 있다.
  • 배치 토폴로지(deployment topology): 다양한 바이너리가 컴퓨터 네트워크에 배치되는 형태
  • 바이너리를 구동하는 컴퓨터는 성능 특성에 영향을 준다. 따라서 기준 버전은 고성능 컴퓨터에서(혹은 고성능 네트워크에서) 수행하고, 새로운 버전은 느린 컴퓨터에서 수행하여 비교한다면 성능이 낮아진 것처럼 보일 것이다. 그래서 두 버전 모두 동일한 컴퓨터에서 수행해야 한다. 하나의 기기에서 두 버전 모두를 테스트할 수 없는 상황이라면 테스트를 여러 번 수행하여 최댓값과 최솟값을 제거해 보정하는 방법도 있다.

14.4.4 배포 설정 테스트

  • 테스트 특성
    • SUT: 밀폐된 단일 머신 혹은 격리된 클라우드에 배포
    • 데이터: 없음
    • 검증 방식: 단정문(비정상 종료는 하지 않음)
  • 결함의 원인이 소스 코드가 아니라 데이터 파일, 데이터베이스, 옵션 등의 설정에 있는 경우도 많다.
  • SUT가 구동될 때 이러한 설정들을 읽어 반영하므로 더 큰 테스트에서는 SUT와 설정 파일을 통합해 테스트해야 한다.
  • 추가 데이터나 검증이 많이 필요 없는 스모크 테스트(smoke test)에 해당한다. SUT가 제대로 구동되면 테스트 통과이고, 그렇지 않으면 실패이다.

14.4.5 탐색적 테스팅

  • 테스트 특성
    • SUT: 프로덕션 혹은 공유 스테이징 환경에 배포
    • 데이터: 프로덕션에서 수집 혹은 알려진 테스트 시나리오 데이터
    • 검증 방식: 수동
  • 탐색적 테스팅(exploratory testing): 새로운 사용자 시나리오를 시도해가며 의문스러운 동작을 찾는 수동 테스트
  • 시스템을 관통하는 새로운 실행 경로로 시험해보며 예상이나 직관과 다르게 동작하는지 혹은 보안 취약점은 없는지를 찾는다.
  • 탐색적 테스팅은 새로운 시스템은 물론 이미 서비스 중인 시스템에서도 예상치 못한 동작과 부작용을 발견해낼 수 있어 유용하다.
  • 테스터에게 새로운 실행 경로를 시도해보게 하여 테스트 커버리지를 높이고, 버그를 찾게 되면 해당 경로를 자동화된 기능 테스트로 만들어 활용한다. 이런 면에서 기능 통합 테스트의 퍼즈 테스트(fuzz test)와 비슷하다.
    • 퍼즈 테스트(fuzz test): 정상적이지 않은 무작위의 데이터를 입력해 수행하는 테스트

한계

  • 수동 테스트는 선형으로 확장되지 않는다. 다시 말해 수동 테스트를 수행하려면 사람이 시간을 써야 한다. 그러므로 탐색적 테스팅에서 발견한 모든 결함은 자동 테스트로 만들어서 다음 번에는 사람이 수행하지 않도록 해야 한다.

버그 파티

  • 구글은 탐색적 테스팅에 주로 버그 파티(bug bash)를 활용한다. 엔지니어는 물론 관련된 모든 사람이 모여 제품을 수동으로 테스트한다.
  • 버그 파티마다 집중적으로 살펴볼 영역이나 시작점을 미리 정해둘 수 있다. 하지만 기본 목표는 다양한 상호작용을 충분히 일으켜서 의심스러운 동작이나 확실한 버그를 찾아 기록하는 것이다.

14.4.6 A/B 차이 회귀 테스트

  • 테스트 특성
    • SUT: 두 개의 격리된 클라우드에 배포
    • 데이터: 대체로 프로덕션 환경에서 복사한 혹은 샘플링한 데이터
    • 검증 방식: A/B 차이 비교
  • 출시된 제품에 잠재된 결함 중 많은 수는 예측하는 게 불가능하다.
  • 하이럼의 법칙이 말해주듯, 공개 API는 제공자가 안내하는 대로만 쓰이는 게 아니다.
  • A/B 차이 테스트는 구버전 제품과 신버전 제품의 공개 API로 트래픽을 보내 둘의 반응이 어떻게 다른지를 비교한다(특히 마이그레이션 시 활용).
  • 모든 차이는 기대한 반응과 기대하지 않은 반응(회귀)으로 구분한다.
  • 이 때 SUT는 두 벌이 필요하다. 하나는 기준이 되는 바이너리들로 구성되고, 다른 한 벌은 변화를 준 바이너리들로 구성된다. 그리고 제3의 바이너리가 트래픽을 보내고 결과를 비교하는 역할을 한다.
  • 변형된 A/B 테스트도 있다. 예를 들어 (시스템을 자기 자신과 비교하는) A-A 테스트는 비결정적인 동작과 노이즈 등의 일관되지 않은 동작을 찾는 데 활용한다. 또한 A/B 테스트의 결과 중 A 혹은 B 자체의 불규칙한 동작 때문에 발생한 차이를 식별해내는 데도 쓴다.
  • 구글에서는 최동 프로덕션 버전과 베이스라인 버전에 더하여, 대기 중인 변경들도 미리 반영한 버전까지 총 3가지를 비교하는 A-B-C 테스트도 활용한다. 이 테스트는 즉각 반영하려믄 변경의 영향뿐 아니라 다음 버전에 반영할 변경까지의 누적된 영향을 한눈에 쉽게 볼 수 있게 해준다.
  • A/B 차이 테스트는 출시된 시스템에서 예상치 못한 부작용을 찾아낼 수 있는 저렴하면서도 자동화 가능한 방법이다.

한계

인가
  • 누군가는 어떤 유의미한 차이가 생겼는지를 알아챌 만큼 결과를 이해할 수 있어야 한다.
  • 일반적인 테스트와 달리 발견된 차이가 좋은 것인지 나쁜 것인지(혹은 기준 버전의 결과가 맞는 것인지) 명확하지 않기 때문에 테스트 중간에 종종 사람이 개입해야 한다.
노이즈
  • 예상치 못한 노이즈로 인한 차이가 결과에 더해진다면 또다시 사람이 직접 조사해봐야 한다.
  • 노이즈들은 반드시 찾아 제거해야 하며 훌륭한 A/B 차이 테스트를 구축하는 데 커다란 걸림돌로 작용한다.
커버리지
  • 충분히 의미 있는 트래픽을 생성하기 어려울 수 있따. 특이 케이스를 찾아낼 수 있는 시나리오를 만들어주는 테스트 데이터가 필요한데, 이런 데이터를 수동으로 관리하기란 쉽지 않다.
설정(setup)
  • SUT 하나도 설정하고 관리하기가 만만치 않다. 한 번에 다룰 SUT가 두 개라면 두 배로 복잡해지며 특히 SUT들이 공유하는 인프라가 있다면 더욱 골치이다.

14.4.7 사용자 인수 테스트(UAT)

  • 테스트 특성
    • SUT: 밀폐된 단일 머신 혹은 격리된 클라우드에 배포
    • 데이터: 수동 생성
    • 검증 방식: 단정문
  • 특정한 최종 고객 혹은 고객의 대리인(고객 대표단이나 제품 관리자 등)이 있는 경우, 사용자 인수 테스트(user acceptance testing, UAT)는 공개 API를 통해 제품을 조작하면서 특정 사용자 여정(user journey)이 의도한 대로 이루어지는지를 보장하는 테스트이다.
  • 소위 '실행 가능한 명세'라 하여, 사용자에게 친숙한 언어로 UAT를 쉽게 작성하고 읽을 수 있도록 해주는 테스트 프레임워크도 다수 존재한다(Cucumber, RSpec 등).
  • 구글은 자동화된 UAT를 잘 활용하지 않으며, 명세 언어 역시 많이 쓰지 않는다. 역사적으로 구글 제품 상당수는 소프트웨어 엔지니어들이 자신들에게 필요해서 만들어냈기 때문이다.

14.4.8 프로버와 카나리 분석

  • 테스트 특성
    • SUT: 프로덕션에 배포
    • 데이터: 프로덕션에서 수집
    • 검증 방식: 단정문과 (지표상의) A/B 특성
  • 프로버와 카나리 분석은 프로덕션 환경 자체가 건강함을 보장하는 수단이다. 이러한 측면에서 프로덕션 모니터링의 한 유형이지만, 구조적으로는 다른 큰 테스트들과 매우 비슷하다.
  • 프로버(Prober: 탐사 장치, 무인 우주 탐사선): 프로덕션 환경을 대상으로 단정문을 수행하는 기능 테스트
    • 시간이 흘러 프로덕션 데이터가 변경되어도 단정문이 지켜지는, 잘 알려지고 결정적인 읽기 전용 동작이 검증 대상이다. 예를 들어, 구글에 접속하여 검색을 시도하고 결과가 반환되는지만 확인한다. 프로덕션 시스템을 스모크 테스트한다고도 볼 수 있지만, 잘 운영되던 서비스가 갑자기 다운되는 등의 심각한 문제를 발생 즉시 알려준다.
  • 카나리 분석(canary analysis)
    • 프로버와 유사하지만 신버전을 프로덕션 환경에 언제 배포할지가 주된 관심사라는 점이 다르다.
    • 프로덕션 서비스 중 일부를 새로운 버전(카나리아)으로 조금씩 대체해가면서 신버전과 기존 버전 모두를 대상으로 프로버를 수행한다. 그리고 각 버전들에서 얻은 상태 지표를 비교하여 문제가 발생하지 않는지 확인한다.
  • 프로버는 서비스 중인 시스템 모두에 수행해야 한다.
  • 신버전을 일부 기기에만 배포하는 단계(카나리 단계)가 프로덕션 배포 프로세스에 존재한다면 해당 단계에서는 카나리 분석을 수행해야 한다.

한계

  • 프로덕션 환경에서 이루어지므로 문제가 포착됐다는 것은 이미 최종 사용자에게 영향을 주고 있음을 뜻한다.
  • 프로버가 만약 값을 변경하는 동작(쓰기)을 수행하면 프로덕션의 상태가 달라진다. 그 결과는 '비결정적인 동작과 단정문 실패', '다음번 쓰기 시 실패', '사용자에게 영향을 주는 부수 효과' 중 하나로 귀결된다.

14.4.9 재해 복구와 카오스 엔지니어링

  • 테스트 특성
    • SUT: 프로덕션에 배포
    • 데이터: 프로덕션에서 수집 혹은 사용자가 제작(결함 주입)
    • 검증 방식: 수동 및 (지표상의) A/B 차이
  • 재해 복구와 카오스 엔지니어링은 시스템이 예기치 못한 변경이나 실패에도 얼마나 굳건히 대응하는가를 확인하는 테스트이다.
  • 구글은 매년 DiRT(Disaster Recovery Testing)라는 재해 복구 테스트 워 게임을 운영해왔다. 이 게임 기간에는 구글 인프라에 거의 전 지구적 재난 규모의 결함이 주입된다.
    • 데이터센터 화재부터 악의 적인 공격까지 모든 것을 시뮬레이션한다.
    • 지진이 일어나서 마운틴 뷰에 있는 구글 본사가 그 외 모든 구글 인프라에서 완전히 격리된 상황을 시뮬레이션하기도 했다.
    • 단지 기술적인 단점뿐 아니라 핵심 의사결정권자 모두와의 연락이 끊긴 상황에서 회사를 운영하는 데에는 무엇이 부족한가까지 확인할 수 있다.
  • 카오스 엔지니어링은 기술 인프라에 대한 '지속적 테스트'에 가깝다.
    • 카오스 엔지니어링(chaos engineering): 시스템에 꾸준히 결함을 심어서 무슨 일이 벌어지는지 관찰하는 테스트
    • 어떤 결함은 꽤나 심각할 수도 있지만, 카오스 테스트 도구는 대부분의 경우 수습할 수 없는 지경에 이르기 전에 기능을 복원하도록 설계되었다.
    • 카오스 엔지니어링의 목표는 시스템이 안정적이고 신뢰할 수 있다는 담당 팀의 가정을 깨줘서 시스템에 복원력이 갖춰지도록 돕는 것이다.
    • 오늘날 구글 팀들은 Catzilla라는 자체 제작 시스템을 이용해 매주 수천 번의 카오스 테스트를 수행한다.
  • 이런 부류의 결함 테스트와 부정적 테스트를 수행하려면 운영 중인 시스템이 (이론적으로라도) 테스트를 견딜 수 있는 내결함성(fault tolerance)을 갖추고 있어야 한다. 혹은 테스트 자체의 비용과 위험을 감내할 수 있는 시스템에 적합하다.

한계

  • 프로덕션 환경에서 이루어지므로 문제가 포착됐다는 것은 이미 최종 사용자에게 영향을 주고 있음을 뜻한다.
  • DiRT는 비용이 상당히 들기 때문에 구글도 일정을 잘 조율해서 드물게 수행한다. 이러한 수준으로 운영이 중단된다면 테스트 기간 동안 실제로 회사 차원에서 커다란 고통이 따르고 지원들의 생산성도 떨어진다.

14.4.10 사용자 평가

  • 테스트 특성
    • SUT: 프로덕션에 배포
    • 데이터: 프로덕션에서 수집
    • 검증 방식: 수동 및 (지표상의) A/B 차이
  • 사용자가 서비스를 어떻게 이용하는지에 관한 많은 데이터를 수집할 수 있다.
  • 사용자 인수 테스트(UAT)의 대안이 될 수 있다.

개밥 주기(dogfooding, 사내 시험 적용)

  • 공개 대상을 제한하는 식으로 프로덕션 환경에서 일부 사용자가 새로운 기능을 맛보도록 할 수 있다.
  • 구글은 종종 직원들을 대상으로 진행하는데, 실제 프로덕션 환경에서 전달되는 아주 값진 피드백을 얻게 된다.

실험(experiment)

  • 새로운 기능을 일부 사용자에게 제공하되 그 사실을 알리지 않고 진행한다. 그런 다음 원하는 지표를 기준으로 실험집단과 통제집단을 비교한다 (유튜브에서 싫어요 수를 보여주지 않는 형태로 좋아요 메커니즘을 변경).
  • 실험은 구글에 매우 중대한 테스트 수단이다.

평가자 감정

  • 변화된 결과를 인간 평가자(rater)들에게 보여주고 어느 것이 더 나은지와 왜 그런지를 선택하게 한다. 이 피드백은 해당 변경이 긍정적인지, 중립적인지, 부정적인지를 결정하는 데 쓰인다.
  • 평가자 감정은 비결정적인 시스템, 예를 들어 명백한 정답 없이 더 좋거나 나쁜 개념만 존재하는 머신러닝 시스템에 매우 중요하다.

14.5 큰 테스트와 개발자 워크플로

  • 대규모 테스트는 누가 작성하는가? 테스트를 수행하고 문제를 분석하는 사람은 누구인가? 테스트의 소유자는 누구고 시스템이 문제 상황을 견뎌내게 만드는 책임은 누구에게 있는가?
  • 서브밋 전과 후에 자동 수행되는 메커니즘 갖추기가 중요하다. 단위 테스트용과는 다른 메커니즘이라도 상관없다.
    • 구글에서도 큰 테스트 상당수가 지속적 빌드 시스템인 TAP을 활용하지 못한다.
    • 큰 테스트는 영구적이지 않고, 심하게 불규칙하며, 많은 경우 자원을 너무 많이 쓴다. 하지만 이 테스트들이 계속 정상 수행되도록 유지해야 한다. 그렇지 않으면 아무런 신호도 보내주지 않고 분류하기도 너무 어려운 문제를 일으킨다.
  • 포스트서브밋 단계에서 큰 테스트들을 자동 수행하는 별도의 지속적 빌드를 갖춘다.
    • 구글은 이 테스트들을 프리서브밋 때에도 수행하도록 독려한다. 그래야 작성자에게 직접. 피드백이 가기 때문이다.
  • 사람에 의한 평가가 개입되는 A/B 차이 테스트 역시 개발자 워크플로에 녹일 수 있다.
    • 프리서브밋 테스트인 경우 변경을 승인하지 건에 코드 리뷰 시 UI의 차이를 확인하게끔 요구할 수 있다.
    • 구글은 이러한 프리서브밋 테스트의 일환으로, 제출된 코드에 명시적으로 승인받지 않은 UI 변경이 포함되어 있다면 자동으로 버그가 등록되고 릴리즈가 중단되게 했다.
  • 너무 거대하고 고통스러운 프리서브밋 테스트는 개발자들이 매우 곤혹스러워 한다. 이런 테스트는 포스트서브밋 방식으로 수행하고 릴리즈 프로세스에도 포함시켜 또 실행한다.
  • 프리서브밋 방식을 포기할 때의 단점은 결함이 모노리포까지 배포될 수 있고, 그렇게 되면 문제를 일으킨 변경을 찾아내어 롤백해야 한다는 것이다. 그래서 개발자의 고통과 늦어지는 변경 반영 시간과 지속적 빌드의 신뢰성 사이에서 절충점을 찾아야 한다.

14.5.1 큰 테스트 작성하기

  • 큰 테스트의 구조는 표준화가 제법 잘 되어 있지만, 실제로 구현하는 데는 여전히 어려움이 많다.
  • 큰 테스트를 작성할 때 가장 좋은 방법은 명확한 라이브러리, 문서자료, 예시 코드를 참조하는 것이다.
  • 구글은 단위 테스트에서 쓰는 단정문 라이브러리를 기능 통합 테스트에서도 쓴다. 하지만 SUT와의 상호작용, A/B 테스트, 테스트용 시드 데이터 생성, 테스트 워크플로 조직 등에 필요한 라이브러리들도 시간을 들여 따로 구축해왔다.
  • 더 큰 테스트를 관리하려면 투입되는 시간과 자원 모든 면에서 비용이 많이 든다.
  • A/B 차이 테스트가 인기인 이유 하나는 검증 단계 관리에 사람이 신경을 덜써도 되기 때문이다.
  • 프로덕션 SUT들로 바로 테스트하면 격리된 밀폐 SUT를 따로 운영할 때보다 비용이 절약된다.
  • 하지만 비용은 전체적으로 봐야 한다. 차이를 수동으로 일치시키거나 프로덕션 환경에서 바로 테스트할 때의 위험 비용이 절약한 비용보다 크면 다 아무 의미 없다.

14.5.2 큰 테스트 수행하기

테스트 속도 개선하기

  • 엔지니어들은 느린 테스트를 기다려주지 않는다. 테스트가 느릴수록 엔지니어가 테스트를 수행하는 빈도가 줄어들어서, 실패하는 테스트가 나와도 수정되어 성공으로 바뀌기까지의 시간이 길어진다.
  • 테스트의 속도를 높이는 가장 좋은 방법은 테스트 범위를 줄이거나 더 작은 테스트로 나눠 병렬로 수행하는 것이다.
  • 테스트가 실제 이용자ㄷ와 동일하게 작성하는 게 가장 좋다.
    • 마이크로초 단위로 상태 변화를 폴링(polling)하면서 원하는 작업이 완료됐는지 확인한다. 타임아웃을 두어 테스트가 일정 시간 후 다음 단계로 안전하게 넘어갈 수 있도록 해주는 게 좋다.
    • 이벤트 핸들러를 구현한다.
    • 이벤트 완료를 알려주는 알림 시스템에 등록(subscribe)한다.
  • sleep()과 타임아웃에 의존하는 테스트는 때마침 시스템에 과부하가 걸리면 줄줄이 실패하게 된다. 그러면 이 테스트들을 더 자주 수행해야 해서 부하가 더 커지는 악순환이 시작된다.
내부 시스템 타임아웃과 지연 낮추기
  • 프로덕션 시스템의 설정은 일반적으로 분산 시스템에 배포된다고 가정하고 만들지만 각 SUT는 하나의 기기 (혹은 같은 지역에 국한된 하나의 클러스터)에 배포될 수도 있다. 그래서 프로덕션 코드가 타임아웃 값을 하드코딩하여 사용하거나 (특히) sleep()을 사용한다면 테스트를 수행할 때는 타임아웃 값을 줄일 수 있게 수정해야 한다.
테스트 빌드 시간 최적화
  • 요소들이 의존하는 다른 바이너리들은 최신 코드로 매번 새로 빌드하기보다는 이미 빌드되어 있는 안정된 버전을 사용하는 게 낫다.
  • 프로덕션 시스템은 모듈별로 다른 버전을 배포해 운영하므로 이 방식이 현실을 더 잘 반영한다고 말할 수 있다.

불규칙한 결과에서 벗어나기

  • 불규칙한 결과는 단위 테스트에서도 당연히 나쁘지만 더 큰 테스트에서 나타난다면 테스트 자체를 활용하기 어렵게 한다.
  • 담당 팀은 불규칙성을 제거하는 업무의 우선순위를 높여 대응해야 한다.
  • 불규칙성을 최소화하려면 가장 먼저 테스트 범위를 줄여야 한다. 밀폐된 SUT라면 다중 사용자 문제 혹은 프로덕션 환경이나 공유 스테이징 환경에서 벌어지는 실제 불규칙성 문제를 걱정하지 않아도 된다.
  • 기기 한 대만으로 구동되는 밀폐된 SUT라면 분산된 SUT에서 벌어지는 네트워크 문제나 배포 불규칙성 문제도 사라진다.
  • 때에 따라서는 테스트 속도와 불규칙한 결과 사이에서 절충점을 찾아야 한다.
    • 테스트를 반응형이나 이벤트 드리븐 방식으로 만들면 속도도 높이고 불규칙성도 없앨 수 있다.
    • 일정 시간 sleep()을 하려면 타임아웃 시간을 관리해야 하는데, 타임아웃 값은 테스트 코드에 적혀있을 것이다.
    • 시스템이 비결정적이라면 내부 시스템의 타임아웃 값을 키우면 불규칙성이 줄고, 반대로 타임아웃 값을 줄이면 불규칙성은 커질 것이다.
    • 여기서 핵심은 시스템이 최종 사용자가 감내할 수 있는 수준에서 동작하면서 불규칙한 테스트도 제대로 수행되도록 하는 절충점을 찾는 것이다.

이해되는 테스트 만들기

  • 테스트 엔지니어가 이해할 수 없는 결과를 낳는 테스트는 개발자 워크플로에 통합하기가 특히 더 어렵다.
  • 단정문을 사용하는 테스트라면 성공/실패 여부를 명확히 알려줘야 하며, 실패의 원인을 쉽게 분류해낼 수 있도록 의미 있는 오류 메시지를 출력해줘야 한다.
  • A/B 차이 테스트처럼 사람이 판단해야 하는 테스트라면 특별한 처리가 필요하다. 그렇지 않으면 프리서브밋 테스트에서 무시하고 건너뛸 위험이 있다.

큰 테스트에서 실패를 잘 처리하는 요령

무엇이 실패했는지 명확히 알려주자
  • 최악의 시나리오는 단정문에 실패했습니다라는 메시지와 스택 추적만 달랑 출력하는 것이다.
  • '검색 결과 10개를 예상했으나 1개만 반환됨'처럼 맥락 정보를 제공해야 한다.
  • 성능 테스트나 A/B 차이 테스트라면 결과에서 무엇을 측정해야 하는지와 동작이 의심되는 이유를 명확히 설명해줘야 한다.
최소한의 노력으로 근본 원인을 찾을 수 있도록 하자
  • 호출 체인이 여러 프로세스 경계를 넘나들 수 있기 때문에 스택 추적은 더 큰 테스트에는 유용하지 못하다. 대신 호출 체인을 추적할 수 있도록 해주거나 범위를 좁힐 수 있는 자동화에 투자해야 한다.
지원 정보 및 연락처 정보를 제공하자
  • 테스트 소유자와 지원 책임자에게 쉽게 연락할 수 있도록 하여 테스트 수행자가 필요할 때 도움을 청할 수 있어야 한다.

14.5.3 큰 테스트의 소유권

  • 더 큰 테스트에는 반드시 소유자가 문서로 기록되어 있어야 한다. 여기서 소유자란 테스트가 변경될 때 검토해주고 테스트가 실패했을 때 지원해줄 수 있는 사람들 뜻한다.
  • 소유권이 불분명하면...
    • 기여자가 테스트를 수정하거나 개선하기가 더 어려워진다.
    • 테스트 실패 시 해결되기까지 더 오래 걸린다.
  • 특정 프로젝트의 구성요소들을 검증하는 통합 테스트라면 소유권은 프로젝트 리드가 맡아야 한다.
  • 기능에 중점을 둔 테스트(여러 서비스에 걸친 비즈니스 기능을 검증하는 테스트)라면 해당 기능의 소유자(feature owner)가 테스트도 소유해야 한다. 이때 기능 소유자는 해당 기능의 종단간 구현을 책임지는 소프트웨어 엔지니어일 수도 있고, 제품관리자 혹은 해당 비즈니스 시나리오를 소유한 테스트 엔지니어일 수도 있다.
  • 소유자가 누구든 테스트의 전반적인 건실성을 보장하고 유지보수를 지원하고 보상을 줄 수 있는 권한 모두를 갖춘 사람이어야 한다.

일반적인 코드 소유권

  • 대다수 더 큰 테스트는 독립 실행형 코드 형태로 만들어지며, 코드베이스상의 약속한 특정한 위치에 둔다. 이 경우 모노리포에 이미 존재하는 OWNERS 정보를 치용하여 해당 테스트 코드의 소유자가 테스트의 소유자임을 자동으로 알 수 있다.

테스트별 애너테이션

  • 테스트 클래스나 모듈 하나에 테스트 메서드가 여러 개인 경우가 있는데, 이 때 각 테스트 메서드를 책임지는 기능 소유자는 다를 수 있다.
  • 이런 경우 각 언어에 맞는 애너테이션을 이용하여 각 메서드의 테스트 소유자를 명시한다. 그러면 테스트 메서드 실패 시 누구에게 연락해야 할지를 바로 알 수 있다.

14.6 마치며

  • 종합적인 테스트 스위트라면 대상 시스템에 충실해야 하고, 그러려면 단위 테스트가 다루기 어려운 문제를 검증해주는 더 큰 테스트가 필요하다. 이런 테스트는 필연적으로 더 복잡하고 느리다.
  • 따라서 소유자 관리, 유지보수, 적시에 수행되고 있는지(가령 프로덕션에 배포하기 전) 등에도 더 신경써야 한다.
  • 더 큰 테스트라고 해도 (충실성을 유지하면서) 되도록 작게 만들어서 개발자 워크플로에 부드럽게 녹여야 한다는 원칙은 변하지 않는다.
  • 시스템의 위험 요소를 찾아내는 종합적인 테스트 전략과 이를 뒷받침하는 더 큰 테스트는 대부분의 소프트웨어 프로젝트에 반드시 필요하다.

14.7 핵심 정리

  • 더 큰 테스트는 단위 테스트가 다루지 못하는 문제를 책임진다.
  • 더 큰 테스트는 테스트 대상 시스템, 데이터, 동작, 검증으로 구성된다.
  • 위험을 식별해주는 테스트 전략과 그 위험을 완화해줄 더 큰 테스트까지 포함해야 좋은 설계다.
  • 더 큰 테스트가 개발자 워크플로에 마찰 없이 녹아들도록 관리하려면 더 많이 노력해야 한다.