구글도 초창기에는 엔지니어에 의한 자동 테스트를 그다지 중요하게 생각하지 않았다. 그러나 2005년에 구글 웹 서버(GWS) 규모와 복잡성이 무척 커지면서 생산성이 급속도로 떨어지는 경험을 했다. GWS는 구글 검색 쿼리를 제공하는 웹 서버로, 구글 검생에 있어서는 마치 공항의 관제 시스템만큼 중요한 역할을 하는 서비스이다. 릴리즈 때마다 버그가 넘쳐났고 다음 릴리즈까지의 길이는 무한정 늘어났다. 서비스를 수정해야 할 때마다 팀원들은 불안해 했고 프로덕션 환경에서만 기능에 영향을 주는 버그도 너무나 자주 발생했다.
이때 당시의 구글 테크리드는 엔지니어 주도의 자동 테스트를 정책 차원에서 도입하기로 결정했다. 결과적으로 1년만에 긴급하게 코드를 수정해 배포하는 건수가 '절반'으로 줄었고 이와 동시에 연중 처리 완료하는 이슈 개수도 계속해서 증가하는 추세가 되었다.
이때의 경험을 바탕으로 구글은 전사적으로 테스트 작성 문화를 정착시키려고 모든 노력을 총 동원했고
이 과정에서 배운 다양한 노하우를 이 책의 11장을 통해서 공유한다.
Green-Red-Refactor 라고도 부르는 TDD 방법론이 생각나는 소제목이다.
테스트 자동화를 위한 과정에서
테스트 작성은 프로세스의 첫 단계일 뿐이다. 테스트를 작성하자마자 바로 테스트를 실행해서 검증해야 한다. 테스트 코드는 수시로 실행해야 한다. 자동 테스트의 핵심은 같은 동작을 끊임없이 반복하는 데 있다. 그리고 이 실행결과에서 실패하는 테스트가 있으면 그 즉시, 수 분 내로, 또는 최대한 빨리 그 실패를 해결해야만 한다. 그렇지 않고 실패한 테스트를 그대로 둔다면 팀원들은 점점 테스트를 신뢰하지 못하게 된다. 테스트를 신뢰하지 못하면 테스트를 실행하는 일도 점차 줄어들고 결국에는 테스트 작성을 멈추게 된다.
건실한 자동 테스트 문화를 만들고 모두가 테스트를 작성하고 공유하도록 장려해야만 한다. 테스트를 정기적으로 실행해야 한다. 마지막으로 가장 중요한 것은 테스트가 실패하면 곧장 조치하도록 권장해야 테스트 프로세스를 신뢰하고 계속 이어나갈 수 있다.
모든 테스트는 독립적이어야 한다.
모든 테스트 케이스는 그 자체로 완전해야 한다.
테스트 코드에는 행위를 수행하는 데 필요한 정보'만'을 포함해야 한다.
테스트 코드에 조건문, 반복문이 있으면 안 된다.
테스트 코드는 오직 실패했을 때만 들여다 본다는 사실을 기억하자
조건1: 단 하나의 프로세스에서 실행하는 테스트. 심지어는 하나의 스레드로까지 범위를 좁히기도 한다.
조건2: sleep, I/O 연산 사용 금지. 대신에 테스트 대역(test double) 사용.
테스트 스위트가 하루종일 실행하는 환경을 구축하려면
테스트 실행이 아주 빠른 속도로 완료할 수 있도록 설계해야 하기 때문이다.
특징1: 여러 프로세스, 여러 스레드 활용 가능
특징2: 로컬 호스트와 네트워크 통신 허용(예, 웹 드라이버(Web Driver))
무엇이든지 가능
더 유연해지는 만큼 위험도 늘어남. 여러 기기에 걸쳐 있는 시스템을 네트워크로 연결해 다루게 되면서 단일 기기에서 구동할 때보다 느려지거나 비결정적으로 동작할 가능성이 훨씬 높아진다.
구글이 큰 테스트를 사용하는 경우는 (1) 종단간(end-to-end) 테스트, (2) 레거시 컴포넌트를 테스트할 때(테스트 대역 사용 불가능)
성공할지 실패할지 명확하지 않은 테스트 코드
테스트당 실패 확률이 0.1% 밖에 안 되더라도 테스트를 하루에 1만 번 수행한다면 매일 평균 10번씩은 실패 원인을 조사해서 조치해야 한다. 팀에 당장 필요한 보다 생산적인 일을 할 시간은 그만큼 줄어든다.
불규칙한 테스트 결과가 반복적으로 나타날 때 발생할 수 있는 더 큰 문제점은 팀원들이 테스트 코드를 믿지 못하게 되는 상황이다. 엔지니어들은 테스트가 실패하더라도 더는 신경 쓰지 않는다.
대부분의 불규칙한 실패는 테스트 자체에 비결정적으로 동작하는 로직이 있어서 발생한다. 소프트웨어 클록 시간, 스레드 스케쥴링, 네트워크 지연시간, 하드웨어 인터럽트, 브라우저 렌더링 엔진 등의 요소가 대표적인 예시이다. 이런 로직을 테스트 코드에서 제거해야 한다.
테스트 범위는 테스트가 얼마나 많은 코드를 검증하는지를 가리키는 말이다.
좁은 범위 테스트(예, 유닛 테스트)는 독립된 클래스나 메서드같이 코드 베이스 중 작은 일부 로직을 검증하도록 설계한다.
중간 범위 테스트(예, 통합 테스트)는 적은 수의 컴포넌트 사이의 상호작용을 검증하도록 설계한다. 서버와 데이터베이스 상호작용이 예시이다.
넓은 범위 테스트(예, 기능 테스트, 종단간 테스트, 시스템 테스트)는 클래스나 메서드 하나만 실행할 때는 괜찮지만 여럿을 조합해 실행했을 때 나타나는 예기치 못한 동작을 검증하도록 설계한다.
[그림 11-3]
구글은 되도록 작은 테스트를 추구하며, 마찬가지로 좁은 범위 테스트를 추구한다.
실제로 구글은 [그림 11-3] 같은 비율로 테스트를 작성한다.
[그림 11-4]
아이스크림콘이나 모래시계 형태의 테스트 비율을 안티패턴이므로 피하는 것이 좋다.
물론 위의 수치는 절대적인 정답은 아니다. 회사마다 프로젝트마다 상황에 따라서 우선순위는 다르게 적용하면 된다. 연동 작업을 좀 더 확실하게 테스트할 필요가 있다면 통합 테스트, 종단간 테스트를 더 많이 작성해도 된다.
실패는 시스템이 고려해야 하는 가장 중요한 상황 중 하나이다. 언젠가는 실패가 찾아온다. 그러나 시스템이 재난에 잘 대응하는지를 보기 위해 실제로 재난이 일어날 때까지 기다리는 것은 어리석다. 흔한 유형의 실패 상황을 시뮬레이션하는 자동 테스트를 작성하면 된다. 신뢰할 수 있는 시스템이라면 부정적인 조건을 예측하고 대응 방식을 통제할 수 있어야 한다.
시스템이 제대로 테스트되었는지를 판가름하는 지표로는 적합하지 않다.
코드베이스가 커지다 보면 기존 코드를 변경하는 일은 피할 수 없다. 그러나 자동 테스트가 엉망으로 작성되어 있다면 코드를 변경하기 어렵다. 특히 깨지기 쉬운 테스트(brittle test), 즉 예상 결과를 너무 세세하게 표현하거나 복잡한 상용구가 덕지덕지한 테스트가 바로 장애물이다. 즉 테스트 코드의 퀄리티도 프로젝트 자체의 코드 퀄리티 만큼이나 중요하다. 이런식으로 깨지기 쉬운 테스트가 만연해지면 엔지니어들은 서서히 테스트를 작성하지 않고 실행하지 않고 조치하지 않기 시작할 것이다.
프로젝트 규모가 성장하면서 개별 메소드는 점점 복잡해지고 테스트 스위트 하나 하나도 점점 커진다. 테스트 스위트를 실행하는데 시간이 오래 걸리기 시작한다. 수행 빈도는 줄어들고 테스트의 가치는 그만큼 줄어든다. 테스트를 더 빠르게 만들기 위해서 구글은 병렬 실행과 하드웨어 스케일 업(scale-up) 등을 실행하지만 이렇게 느린 테스트 케이스의 숫자가 늘어나면 이런 기법들도 힘을 쓰지 못한다.
테스트가 느려지는 원인은 다양하다. 거대한 시스템 전체를 상당 부분 가동해야 한다거나 에뮬레이터 사용, 대량 데이터 처리, 다른 시스템과의 동기화를 위해서 대기 등이 원인이 될 수 있다. 비결정적인 테스트를 처리하기 위해서 사용하는 sleep()이나 setTimeout() 함수를 사용하는 것도 느린 테스트의 원인이 될 수 있다. 초기에는 5초를 소비하던 통합 테스트가 몇 년 후에는 십여 개의 서비스에 의존할 정도로 커지면 5분씩 잡아먹게 된다.
거대한 테스트를 잘 관리하는 비결은 테스트를 존중하는 문화이다. 엔지니어들이 테스트에 자발적으로 관심을 갖도록 장려해야 한다. 훌륭한 기능을 출시했을 때와 마찬가지로 테스트를 견고하게 만든 엔지니어를 보상해야 한다. 적절한 성능 목표를 설정하고 느리거나 중요하지 않은 테스트는 리팩터링한다. 테스트 코드도 제품 코드 만큼 사려깊게 다뤄야 한다. 간단한 코드 변경에도 시간이 제법 소요된다면 테스트를 더 견고하게 만드는 데 노력을 기울여야 한다.
문화를 가꾸는 일과 더불어 린터(linter)를 개발하거나 문서자료를 보강하는 등, 안 좋은 테스트를 만드는 실수를 줄여주는 테스트 인프라에도 투자해야 한다. 지원해야 할 프레임워크나 도구의 수를 줄여서 투자대비 효율을 높여야 한다. 테스트 관리 비용을 낮추는 데 투자하지 않는다면 종국에는 엔지니어들이 테스트가 전혀 가치 없다고 결론내게 될 것이다.
신입을 대상으로 테스트 코드 작성의 중요성을 강조하는 오리엔테이션 수업을 개설했다. 시간이 지나자 구글 전체에서 오리엔테이션 수업을 듣고서 입사한 직원의 수가 아닌 직원의 수를 넘어섰다. 그러면서 자연스럽게 모든 직원들은 테스트 코드를 고려하며 제품 코드를 작성하게 되었다.
테스트 인증(test certified)이라는 인증 프로그램을 만들었다. 테스트 인증의 목적은 각 팀이 자신의 테스트 프로세스 수준(성숙도)를 알게 하고 한 단계 올라서기 위한 지침을 제공하는 것이었다.
총 5단계로 나누어져 있고, 선의의 경쟁을 통해서 사람들이 자발적으로 테스트 코드 품질을 올리기 위해서 노력하도록 유도하는 데 성공했다. 현재는 pH(프로젝트 건실성, Project Health)라는 프로젝트로 대체했다.
구글에서 테스트 문화를 뿌리내리게 하기 위해 한 노력 중에 화장실에서도 테스트(Testing on the Toilet, TotT)만큼 참신한 것은 없을 것이다. 테스트와 관련된 한 페이지짜리 쪽지를 만들어 전세계 모든 구글 오피스 화장실에 붙였다. 처음에는 사적인 공간을 침범했다고 불평하는 목소리도 있었지만 종국에는 불만이 있는 사람이든 좋아하는 사람이든 상관없이 모두가 테스트에 대해서 말하게 되었다는 점에서 이 프로젝트는 성공적이었다고 말할 수 있다.
TotT는 빠르게 구글의 대표적인 문화로 자리 잡았다. 심지어는 잠시 화장실에 갔다가 이 포스트를 발견한 외부 손님들까지도 이에 대해서 흥미로워했다. TotT 에피소드를 살짝 수정한 버전을 공개적으로 게시하기도 한다(https://testing.googleblog.com/search/label/TotT). 이런 방식으로 구글의 경험을 업계 전체와 공유하는 데도 어느 정도 기여했고 TotT는 테스트 관련 활동 중 가장 오래 시행되고 영향력도 가장 컸다.
모든 종류의 테스트를 다 자동화할 수는 없다. 예컨대 검색 결과의 품질을 테스트하려면 사람의 판단이 개입되어야 한다. 구글은 실제로 쿼리를 날리고 결과에 관한 느낌을 기록하는 검색 품질 평가자(Search Quality Raters)라는 연구를 내부적으로 수행한다. 비슷한 예로, 음성이나 영상에서 느껴지는 미묘한 차이는 자동 테스트로 잡아내기 어렵다. 그래서 전화나 통화 시스템의 성능을 평가할 때는 종종 사람에게 판단을 맡긴다. 보안 취약점을 찾아내는 영역 등도 창의성이 필요한 분야이기에 기계보다 인간이 업무를 더 잘 수행한다.
이를 일반화한 용어가 탐색적 테스팅(exploratory testing)이다. 검사 대상을 마치 고장내야 할 퍼즐로 취급하며 예외적인 데이터를 입력하거나 예상치 못한 절차로 조작하여 망가뜨리려 시도한다. 탐색적 테스팅으로 문제를 발견하는 경우에도 즉시 자동 테스트를 추가해서 문제가 재발하지 않도록 예방해야 한다.