Home 14. 더 큰 테스트
Post
Cancel

14. 더 큰 테스트

14.1 더 큰 테스트란?

더 큰 테스트들은 작은 테스트와 많은 면에서 다르다. 따라야 하는 제약도 다르다.

[특성]

  • 느릴 수 있다. 구글에서 대규모 테스트의 기본 타임아웃 값은 15분~1시간이다.
  • 밀폐되지 않을 수 있다. 대규모 테스트는 다른 테스트나 최종 사용자와 자원 및 트래픽을 공유하기도 한다.
  • 비결정적일 수 있다.

이러한 단점들에도 불구하고 더 큰 테스트를 이용하는 이유는 단위 테스트는 호환성 문제가 있는지 확인하는 데 그다지 도움이 안되고, 개별 함수, 객체, 모듈에 대한 확신을 심어준다. 반면 더 큰 테스트는 시스템 전체가 의도대로 동작한다는 확신을 더해주는 역할을한다.

14.1.1 충실성

더 큰 테스트가 존재하는 첫 번째 이유는 바로 충실성(실제 행위를 얼마나 반영했느냐)을 높이기 위함이다. 더 큰 테스트는 환경 충실성의 낮음-높음 사이에서 가장 적합한 지점을 찾아내는 것이다. 충실성이 높아질수록 비용이 커져서 테스트 실패시 입는 손해도 크다.

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

  1. 부정확한 테스트 대역
  2. 설정 문제 - 단위 테스트는 주어진 바이너리 내의 코드를 다룬다. 일반적으로 바이너리는 단독으로 실행될 수 없고, 배포 설정, 시작 스크립트 같은 게 필요할 때가 많다.
  3. 과부하 시 나타나는 문제 - 성능, 부하, 스트레스 테스트는 바이너리에 상당한 양의 트래픽을 일으키므로 통상적인 단위 테스트 모델에 녹이기 어렵다.
  4. 예기치 못한 동작, 입력, 부작용 - 단위 테스트의 범위는 작성자의 상상력에 갇히게 된다. 단위 테스트만으로는 공대 API에 명시되지 않은 행위까지 모두 확인할 가능성이 극히 낮다.
  5. 창발적 행위와 진공 효과

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

좋은 단위 테스트라면 높은 신뢰성, 빠른 속도, 높은 확장성의 특징을 갖는다. 반면 더 큰 테스트에서는 이 특징을 하나도 갖지 못하는 경우도 생긴다. 예)더 큰 테스트는 많은 인프라를 이용하기 때문에 결과가 비결정적일 때가 많다.

그리고 극복해야 할 과제가 두 가지가 더 있다.

  1. 소유권 문제 - 단위 테스트는 소유자가 누구인지 명확하다 더 큰 테스트는 다수의 단위에 걸져 있어 관련 소유자가 많다. 이로인해 시간이 흐를수록 소유권이 더 모호해지고 유지보수의 책임, 테스트 실패시 누가 문제를 해결할지에 대한 문제가 발생한다.
  2. 표준화 부족 - 더 큰 테스트는 작성하고 실행하고 디버깅하기 위한 인프라와 프로세스가 부실하다. 그로인해 미치는 영향이 상당히 크다. 더 큰 테스트는 실행 방식이 너무 다양하여 테스트를 수행하는 표준화된 방식이 없으므로 자연스럽게 인프라의 지원을 받지 못한다.

14.2 더 큰 테스트 @구글

  1. 구글은 테스트 피라미드를 장려했다. 대다수 테스트가 단위 테스트가 되길 원하여 단일 테스트에 집중했다.
  2. 훗날 C/J Build를 대신하여 TAP을 공식 지속적 빌드 시스템으로 도입하였는데 TAP은 특정한 조건을 충족하는 테스트인 경우에만 C/J Build를 대체할 수 있었다.

14.2.1 더 큰 테스트와 수명

더 큰 테스트들은 시간이라는 관점에서 어떤 영향을 줄까? 단위 테스트는 기대 수명이 몇 시간 이상만 되면 충분히 가치가 있다.

더 큰 테스트들은 모두 수명이 더 긴 소프트웨어에 유용하다. 하지만 수명이 길어질수록 주 관심사가 테스트의 유지보수로 옮겨간다.

건강한 상태를 오래 유지하는 핵심은 개발 시작 후 며칠 안으로 단위 테스트를 만들어 테스트 피라미드를 쌓기 시작하는 것이다. 그런 다음 수동 테스트를 자동화된 통합 테스트로 대체해 피라미드 위층으로 올린다. 오랫동안 코드를 건강하게 유지하려면 단위 테스트와 수동 테스트 사이의 간극을 매우는데 소홀해서는 안 된다.

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

규모가 큰 소프트웨어라면 더 큰 테스트가 그만큼 더 필요하고 유용하다. 하지만 작성하고 수행하고 관리하고 디버깅하는 복잡도는 규모가 커질수록 함께 증가한다. (382~383p)

**통합 테스트라 하더라도 가능한 한 작을수록 좋다. 작은 통합 테스트들로 나눠 연결하는 것으로 ‘연쇄 테스트’라는 방법이 있다.

14.3 큰 테스트의 구조

  1. 테스트 대상 시스템 확보
  2. 필요한 테스트 데이터 준비
  3. 대상 시스템을 이용해 동작 수행
  4. 행위 검증

14.3.1 테스트 대상 시스템

대규모 테스트의 핵심은 테스트 대상 시스템(SUT)이다. 대규모 테스트에서의 SUT는 대체로 하나 이상의 독립된 프로세스에서 수행된다.

SUT의 형태는 주로 다음 두 요소에 의해 결정된다.

  • 밀폐성 - SUT는 현재 테스트하려는 기능과 관련 없는 구성요소를 사용하거나 상호작용하지 못해야 한다.
  • 충실성 - SUT는 테스트 중인 프로덕션 시스템을 충실히 반영해야 한다.

그런데 이 두 요소가 충돌할 때가 많다.

  1. 단일 프로세스 SUT - SUT 전체가 하나의 바이너리로 패키징되고, 테스트 코드까지 함께 패키징된다. 충실성 측면에서는 프로덕션의 토폴리지나 설정과 거리가 가장 먼 테스트가 된다.
  2. 단일 머신 SUT - SUT는 독립된 바이너리로 구성되고, 테스트도 별도의 바이너리로 만들어지나 모두가 하나의 머신에서 구동한다.
  3. 다중 머신 SUT - SUT를 여러 머신에 분산시킨다. 단일 머신 SUT보다 충실성이 높지만 여러 머신 사이를 잇는 네트워크가 불안정성을 키워 테스트에 예기치 못한 영향을 줄 가능성이 커진다.
  4. 공유 환경(스테이징과 프로덕션) - SUT를 독립적으로 실행하는 대신 테스트에서 공유 환경을 직접 사용한다. 하지만 공유 환경을 함께 이용중인 다른 엔지니어와 충돌할 수 있고, 테스트 만을 위해 임의로 변경할 수 없다.
  5. 하이브리드 - 어떤 SUT는 혼합된 형태를 띈다.

밀폐된 SUT의 이점

큰 테스트에서 SUT는 테스트 신뢰성을 떨어뜨리고 피드백 시간을 늘리는 주범이 될 수 있다. 예) 프로덕션 환경에서의 테스트는 실제 운영 중인 시스템을 이용한다. 테스트 코드가 프로덕션 환경에 배포될 때까지 대기해야 한다. 즉 환경에 릴리스되는 시점을 테스트 수행자가 직접 통제하지 못한다.

대안으로 거대한 공유 스테이징 환경을 만들고 테스트를 그 안에서 실행하는 방법이 있다. 그래도 여전히 테스트용 코드가 공유 환경에 반영된 후에야 테스트할 수 있다는 한계는 여전하다.

또 다른 안으로 엔지니어가 스테이징 환경에서 사용할 수 있는 시간을 예약해두고 그 시간동안은 계류 중인 코드를 배포하고 테스트를 실행해 볼 수 있도록 하는 팀도 있다.

다음 단계는 클라우드에서 격리된 영역을 만들어내거나 머신을 밀폐할 수 있는 환경을 구축하고 그 안에 SUT를 배포하는 방법이다. 이런 환경이 갖춰지면 충돌 걱정이다 시간 예약없이 코드를 릴리스할 수 있다.

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

테스트를 하다 보면 피해야 할 고통스러운 경계가 존재한다. 프런트엔드와 백엔드가 만나는 경계가 대표적이다. 이 경계를 포괄하는 테스트는 되도록 피해야 한다.

또 다른 경계로 서드파티 의존성을 들 수 있다. 서드파티 시스템은 대체로 테스트를 위한 공유 환경을 따로 제공하지 않을 것이다. 또 서드파티로 보내는 트래팍애 비용이 매겨지는 경우도 있어 실제 서드파티 API를 직접 사용하는 자동 테스트는 권장하지 않는다.

기록/재생 프록시

??? 389~390p

14.3.2 테스트 데이터

대규모 테스트에서는 두 가지 데이터가 필요하다.

  • 시드 데이터 - SUT를 사전 초기화해주는 데이터
  • 테스트 트래픽 - 테스트 수행 과정에서 SUT로 보내는 데이터

SUT의 상태를 테스트 전에 초기화해두는 작업은 대체로 단위 테스트에서 수행하는 셋업보다 훨씬 복잡하다.

  • 도메인 데이터 - 어떠 데이터베이스는 환경 구성용 데이터가 테이블들에 미리 채워져 있어야 한다. 이런 데이터베이스를 이용한다면 적절한 도메인 데이터 없이는 테스트를 제대로 실행할 수 없을 것이다.
  • 현실적인 기준선 - 현실적인 SUT가 되려면 품질과 양적 측면 모두에서 현실적인 데이터셋이 기본으로 갖춰져있어야 할 것이다.
  • 데이터 기록 API
    • 손수 가공한 데이터 - 데이터를 사람이 손수 만들 수 있다.
    • 복사한 데이터 - 실제 데이터를 복사해 쓸 수 있다.
    • 샘플링한 데이터 - 실제 데이터에서 표본을 추출해 사용한다.

14.3.3 검증

SUT가 구동되고 트래픽이 보내졌다면 제대로 작동했는지 검증해야 한다. 검증 방식으로는 다음과 같이 있다.

  • 수동 검증 - 사람이 SUT와 직접 상호작용하며 올바르게 동작하는지 확인
  • 단정문 - 시스템이 의도된 대로 동작하는지 명확히 검사하는 검증 방식이다.
  • A/B 비교 - 두 벌의 SUT를 구동시켜 똑같은 데이터를 보낸 다음 결과를 비교하는 검증 방식이다.

14.4 더 큰 테스트 유형

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

  • SUT : 밀폐된 단일 머신 혹은 격리된 클라우드에 배포
  • 데이터 : 수동 생성
  • 검증 방식 : 단정문

여러 바이너리가 상호작용하는 기능이라면 바이너리가 하나일 때보다 테스트하기가 훨씬 복잡한 게 당연하다. 대표적인 예로 MSA 환경에서 기능 테스트시 관련된 바이너리를 모두 포함한 SUT를 구동시키고 오픈 API를 통해 바이너리 사이의 실제 상호작용을 검증할 것이다.

14.4.2 브라우저와 기기 테스트

서드파티 입장이 되어 프런트엔드를 통해 애플리케이션을 이용하는 테스트는 커버리지를 높여주는 또 다른 수단이 되어준다.

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

  • SUT : 격리된 클라우드에 배포
  • 데이터 : 수동 생성 혹은 프로덕션 환경에서 복사
  • 검증 방식 : 차이 비교(성능 지표)

성능, 부하, 스트레스 테스트를 작은 단위로 진행할 수도 있지만 때로는 외부 API를 써서 동시다발적인 트래픽을 감당할 수 있는지 확인해야 한다.

부하의 스트레스 처리는 시스템에서 매우 시급한 능력이다. 이 복잡한 동작은 시스템의 개별 구성요소가 아니라 전체와 관련이 있다. 따라서 가능한 한 프로덕션과 비슷한 환경에서 수행되어야 한다.

14.4.4 배포 설정 테스트

  • SUT : 밀폐된 단일 머신 혹은 격리된 클라우드에 배포
  • 데이터 : 없음
  • 검증 방식 : 단정문

결함의 원인이 소스코드가 아니라 데이터 파일, 데이터베이스, 옵션 등의 설정에 있는 경우도 많다. SUT가 구동될 때 이러한 설정들을 읽어 반영하므로 더 큰 테스트에서는 SUT와 설정 파일을 통합해 테스트해야 한다.

14.4.5 탐색적 테스팅

  • SUT : 프로덕션 혹은 공유 스테이징 환경에 배포
  • 데이터 : 프로덕션에서 수집 혹은 알려진 테스트 시나리오 테스트
  • 검증 방식 : 수동

새로운 사용자 시나리오를 시도해가며 의문스러운 동작을 찾는 수동 테스트를 말한다. 훈련된 사용자나 테스터가 공개 API를 이용해서 제품을 구동해보는데 이때 시스템을 관통하는 새로운 실행 경로로 시험해보며 예상이나 직관과 다르게 동작하는지 혹은 보안 취약점은 없는지를 찾는다.

탐색적 테스팅은 새로운 시스템은 물론 이미 서비스 중인 시스템에서도 예상치 못한 동작과 부작용을 발견해낼 수 있어 유용하다.

한계 : 수동 테스트는 선형으로 확장되지 않는다. 탐색적 테스팅에서 발견한 모든 결함은 자동 테스트로 만들어서 다음 번에는 사람이 수행하지 않도록 해야 한다.

버그 파티 : 관련된 모든 사람이 모여 제품을 수동으로 테스트한다. 버그 파티마다 집중적으로 살펴볼 영역이나 시작점을 미리 정해둘 수 있다.

14.4.6 A/B 차이 회귀 테스트

  • SUT : 두 개의 격리된 클라우드에 배포
  • 데이터 : 대체로 프로덕션 환경에서 복사한 혹은 샘플링한 데이터
  • 검증 방식 : A/B 차이 비교

구버전 제품과 신버전 제품의 공개 API로 트래픽을 보내 둘의 반응이 어떻게 다른지 비교한다. 모든 차이는 기대한 반응과 기대하지 않은 반응으로 구분한다.

A/B 차이 테스트는 출시된 시스템에서 예상치 못한 부작용을 찾아낼 수 있는 저렴하면서도 자동화 가능한 방법이다.

한계 : 다음의 난관들을 잘 극복해야 한다.

  • 인가 - 어떤 유의미한 차이가 생겼는지 알아챌 만큼 결과를 이해할 수 있어야 한다.
  • 노이즈 - 예상치 못한 노이즈로 인한 차이가 결과에 더해진다면 또다시 사람이 직접 조사해봐야 한다.
  • 커버리지
  • 설정

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

  • SUT : 밀폐된 단일 머신 혹은 격리된 클라우드에 배포
  • 데이터 : 수동 생성
  • 검증 방식 : 단정문

대상 코드의 작성자가 테스트 코드까지 짠다는 점은 단위 테스트의 주요한 특성이다. 그래서 제품이 의도하는 기능을 엔지니어가 잘못 이해했다면 제품 코드뿐 아니라 단위 테스트에도 고스란히 반영될 우려가 매우 크다.

사용자 인수 테스트 공개 API를 통해 제품을 조작하면서 특정 사용자 여정이 의도한대로 이루어지는지를 보장하는 테스트이다.

14.4.8 프로버와 카나리 분석

  • SUT : 프로덕션에 배포
  • 데이터 : 프로덕션에서 수집
  • 검증 방식 : 수동 및 (지표상의) A/B 차이

프로버는 프로덕션 환경을 대상으로 단정문을 수행하는 기능 테스트이다. 시간이 흘러 프로덕션 데이터가 변경되어도 단정문이 지켜지는지, 잘 알려지고 결정적인 읽기 전용 동작이 검증 대상이다.

카나리 분석은 비슷하나 신버전을 프로덕션 환경에 언제 배포할지가 주된 관심사라는 점이 다르다. 프로덕션 서비스 중 일부를 새로운 버전(카나리아)으로 조금씩 대체해 가면서 신버전과 기존 버전 모두를 대상으로 프로버를 수행한다.

한계 : 프로덕션 환경에서 이루어지므로 문제가 포착되었다는 것은 이미 최종 사용자에게 영향을 주고 있음을 뜻한다.

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

  • SUT : 프로덕션에 배포
  • 데이터 : 프로덕션에서 수집 혹은 사용자가 제작(결함 주의)
  • 검증 방식 : 수동 및 (지표상의) A/B 차이

시스템이 예기치 못한 변경이나 실패에도 얼마나 굳건히 대응하는가를 확인하는 테스트이다. 카오스 엔지니어링은 시스템에 꾸준히 결함을 심어서 무슨 일이 벌어지는지 관찰하는 테스트이다.

한계 : 프로덕션 환경에서 이루어지므로 문제가 포착되었다는 것은 이미 최종 사용자에게 영향을 주고 있음을 뜻한다.

14.4.10 사용자 평가

  • SUT : 프로덕션에 배포
  • 데이터 : 프로덕션에서 수집
  • 검증 방식 : 수동 및 (지표상의) A/B 차이

프로덕션에서 수행하는 테스트로는 사용자가 서비스를 어떻게 이용하는지에 관한 많은 데이터를 수집할 수 있다. 다음과 같이 준비 중인 기능의 예상 반응과 우려사항 관련 지표를 수집하는 방법이 몇 가지 있으며 사용자 인수 테스트의 대안이 될 수 있다.

  • 개밥 주기 - 공개 대상을 제한하는 식으로 프로덕션 환경에서 일부 사용자가 새로운 기능을 맛보도록 할 수 있다.
  • 실험 - 새로운 기능을 일부 사용자에게 제공하고 그 사실을 알리지 않고 진행한다.
  • 평가자 감정 - 변화된 결과를 인간 평가자들에게 보여주고 어느 것이 나은지와 왜 그런지를 선택한다.

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

표준 단위 테스트 인프라를 활용하지는 못하더라도 큰 테스트 역시 개발자 워크플로에 통합하는 일은 여전히 매우 중요하다. 한 가지 묘안은 서브밋 전과 후에 자동 수행되는 메커니즘 갖추기이다. 그 다음에는 포스트서브밋 단계에서 큰 테스트들을 자동 수행하는 별도의 지속적 빌드를 갖추는 것이다.

사람에 의한 평가가 개입되는 A/B 차이 테스트 역시 개발자 워크플로에 녹일 수 있다. 프리서브밋 테스트인 경우 변경을 승인하기 전에 코드 리뷰시 UI의 차이를 확인하게끔 요구할 수 있다.

너무 거대하고 고통스러운 프리서브밋 테스트는 개발자들이 매우 곤혹스러워한다. 이런 테스트는 포스트서브밋 방식으로 수행하고 릴리스 프로세스에도 포함시켜 또 실행한다.

14.5.1 큰 테스트 작성하기

큰 테스트를 작성할 때 가장 좋은 방법은 명확한 라이브러리, 문서자료, 예시 코드를 참조하는 것이다.

더 큰 테스트를 관리하려면 투입되는 시간과 자원 모든 면에서 비용이 많이 든다.

14.5.2 큰 테스트 수행하기

  • 테스트 속도 개선하기 - 테스트가 느릴수록 엔지니어가 테스트를 수행하는 빈도가 줄어들어서 실패하는 테스트가 나와도 수정되어 성공으로 바뀌기까지의 시간이 길어진다. 테스트의 속도를 높이는 가장 좋은 방법은 테스트 범위를 줄이거나 더 작은 테스트로 나눠 병렬로 수행하는 것이다.
    • 내부 시스템 타임아웃과 지연 낮추기
    • 테스트 빌드 시간 최적화
  • 불규칙한 결과에서 벗어나기 - 불규칙한 결과는 테스트 자체를 활용하기 어렵게 한다. 불규칙성을 최소화하려면 가장 먼저 테스트 범위를 줄여야 한다.
  • 이해되는 테스트 만들기 - 테스트 엔지니어가 이해할 수 없는 결과를 낳는 테스트는 개발자 워크플로에 통합하기가 특히 더 어렵다. 실패를 잘 처리하는 요령으로 다음과 같이 있다.
    • 무엇이 실패했는지 명확히 알려주자
    • 최소한의 노력으로 근본 원인을 찾을 수 있도록 하자
    • 지원 정보 및 연락처 정보를 제공하자

14.5.3 큰 테스트의 소유권

더 큰 테스트에는 반드시 소유자가 문서로 기록되어 있어야 한다. 소유자는 테스트가 변경될 때 검토해주고 테스트가 실패했을 때 지원해줄 수 있는 사람을 뜻한다. 소유권이 불분명 하면 기여자가 테스트를 수정하거나 개선하기가 더 어려워지고, 테스트 실패 시 해결되기까지 더 오래 걸린다.

14.6 마치며

종합적인 테스트 스위트라면 대상 시스템에 충실해야 하고, 그러려면 단위 테스트가 다루기 어려운 문제를 검증해주는 더 큰 테스트가 필요하다. 소유자관리, 유지보수, 적시에 수행되고 있는지 등에 신경을 써야한다. 더 큰 테스트라도 되도록 작게 만들어서 개발자 워크플로에 부드럽게 녹여야 한다는 원칙은 변하지 않는다.

14.7 핵심 정리

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

13. 테스트 대역

15. 폐기