- 자동 테스트(automated test)는 버그가 몰래 숨어들어 고객을 놀라게 하는 사태를 막아준다.
- 개발 주기에서 버그를 발견하는 시기가 늦어질수록 고치는 비용이 커진다.
- 새로운 기능을 추가하거나, 코드가 더 건실해지도록 리팩터링하거나, 대규모 재설계를 진행하는 상황에서 자동 테스트는 실수를 빠르게 잡아주므로 안심하고 소프트웨어를 변경할 수 있게 해준다.
- 테스트 체계가 잘 갖춰져 있다면 변화를 두려워할 이유가 없다.
- 테스트를 작성하는 행위가 시스템의 설계도 개선해준다. 데이터베이스에 너무 강하게 묶여 있는지, 이 API가 필수 유스케이스를 지원하는지, 시스템이 극단적인 상황들에 잘 대처하는지 등을 확인할 수 있다.
- 자동 테스트를 작성하면 이런 문제들을 개발 주기의 초반에 잡아내게 된다. 그 결과 모듈화가 더 잘되어 미래의 변화에 훨씬 유연한 소프트웨어가 만들어진다.
- '자동 테스트'의 정체는?
- 테스트하려는 단 하나의 행위(주로 메서드나 API)
- 특정한 입력(API에 전달하려는 값)
- 관측 가능한 출력 혹은 동작
- 통제된 조건(하나의 격리된 프로세스 등)
- 시스템에 특정 값을 입력하고, 출력 결과를 확인하여 시스템이 기대한 대로 동작한 것인지를 판단한다.
- 코드베이스의 덩치에 비례하여 테스트 스위트도 커진다. 그 과정에서 테스트 결과가 일관되지 못하거나 느려지는 문제가 나타나기도 한다. 이러한 문제를 해결하지 못하면 테스트 스위트의 존폐가 위태로워진다.
- 테스트가 생산성을 떨어뜨리고 고칠 게 계속 나오거나 결과를 믿을 수 없다면 엔지니어들은 더 이상 테스트를 신뢰하지 않고 우회 방법을 찾으려 할 것이다.
- 테스트는 좋은 제품을 빠르게 만들 수 있게 해줄 뿐 아니라 우리 삶에서 중요한 제품과 서비스의 안전을 보장하는 데도 점점 핵심적인 역할을 하고 있다. 소프트웨어 결함은 단순히 짜증을 일으키는 수준을 넘어서 막대한 금전적 손실을 낳고 심하면 목숨을 앗아가기까지 하기 때문이다.
- 제품 결함 해결을 프로그래머의 능력에만 의존해서는 안 된다.
- 개별 엔지니어가 버그를 심는 빈도는 아주 낮더라도 프로젝트가 커져 팀원이 많아지면 결함 목록은 계속 길어질 것이다.
- 디버깅 방식에서는 버그가 발생할 때마다 엔지니어가 디버거를 실행해 문제를 분석해내야 한다. 자동 테스트와의 엔지니어링 비용 차이는 하늘과 땅만큼 벌어진다.
- 대부분의 소프트웨어는 기능과 지원 플랫폼이 너무 폭증해서 사람이 모든 행위를 수동으로 검증할 수 있는 한계를 아득히 넘어섰다. 그래서 테스트에서의 해법은 '자동화'뿐이다.
- 가장 순수한 형태의 자동 테스트는 '테스트 작성', '테스트 수행', '실패한 테스트에 대한 조치'로 이루어진다.
// Calculator 클래스가 결과가 음수인 계산을 제대로 처리하는지 확인한다.
func test_2에서5를빼면_마이너스3이다() {
let sut = Calculator()
let expectedResult = -3
let actualResult = calculator.subtract(2, 5)
XCTAssertEqual(actualResult, expectedResult)
}
- 오늘날 시스템의 규모와 배포 속도를 따라잡으려면 모든 엔지니어가 테스트도 함께 개발해야만 한다.
- 테스트를 작성하는 것과 좋은 테스트를 작성하는 것은 별개이다.
- 테스트를 코드로 작성하면 다양한 환경에서 수행할 수 있도록 테스트들을 모듈화하기도 좋다.
- 실패하는 테스트가 해결되지 못하고 빠르게 쌓여간다면 테스트에 투자한 노력이 허사가 되니 그렇게 되지 않도록 하는 것이 중요하다.
- 건실한 자동 테스트 문화에서는 모두가 테스트를 작성하고 공유하도록 장려하고, 테스트들을 정기적으로 실행한다. 그리고 테스트가 실패하면 바로 조치하도록 권장해야 테스트 프로세스를 신뢰하고 계속 이어갈 수 있다.
- 테스트에 투자하는 게 개발자 생산성을 향상시키는 이유는?
- 테스트를 거친 후 제출되는 코드는 통상적으로 결함이 적다. 그러므로 그 코드의 존속 기간 전체로 봤을 때 결함이 줄어든다.
- 코드 조각은 수명이 다하는 날까지 여러 팀 또는 자동 코드 유지보수 시스템이 수정할 수도 있다. 그래서 테스트를 한 번 작성해두면 프로젝트가 살아 있는 내내 값비싼 결함을 예방해주고 짜증나는 디버깅에서 해방시켜주는 식으로 지속해서 혜택을 준다.
- 테스트들이 프롲게트의 주요 기능들을 끊임없이 검증해주는 덕에 좋은 테스트들로 무장한 팀은 자신감을 가지고 변경들을 리뷰하고 수용할 수 있다.
- 행위가 달라지지 않는 리팩터링 시에는 테스트 자체는 변경될 필요가 없으므로 적극 권장할 수 있다.
- 한 번에 하나의 행위만 집중해 검증하는 명확한 테스트는 마치 실행 가능한 문서와 같다. 코드가 특정 상황에서 어떻게 동작하는지 궁금하다면 그 상황을 검증하는 테스트를 보면 되기 때문이다.
- 요구사항이 변경되어 새로운 코드가 기존 테스트를 통과하지 못한다면 그 문서자료(테스트)가 이제 낡았음을 알 수 있다.
- 정확성, 극단 상황, 오류 상황 등 다양한 측면에서 코드를 검사해주는 테스트가 준비되어 있다면 리뷰어가 변경된 코드가 제대로 작동하는지를 검증하는 시간을 크게 줄여준다. 각각의 상황을 머릿속에서 일일이 그려보는 대신 해당 테스트를 수행해 통과하는지만 보면 되기 때문이다.
- 새로 작성한 코드의 테스트를 작성하는 일은 해당 코드의 API가 잘 설계되었는지를 시험하는 행위이다.
- 테스트하기 어려운 코드는 너무 많은 역할을 짊어지거나 의존성을 관리하기 어렵게 짜여졌기 때문일 가능성이 크다.
- 잘 설계된 코드라면 모듈화가 잘 되어 있어야 한다. 다른 코드와 강하게 결합되지 않고 특정 역할에 집중해야 한다.
- 설계 문제를 조기에 바로잡는다면 훗날 수정할 때 고생을 덜 한다.
- 건실한 자동 테스트 스위트를 갖춘 팀은 새로운 버전을 릴리즈하며 불안에 떨지 않는다.
- 엔지니어들은 커다란 시스템 규모의 테스트를 작성하는 것을 선호하지만, 이런 테스트는 작은 테스트와 비교하여 느리고 신뢰도가 낮고 디버깅하기도 어렵다.
- 고통을 줄이고자 하는 욕구가 엔지니어들을 점점 더 작은 테스트를 작성하도록 이끌었다. 그러면서 더 작은 테스트가 더 빠르고, 안정적이고, 평균적으로 고통이 적다는걸 깨우쳤다.
- 작다는 것의 의미는?
- 크기(size): 테스트 케이스 하나를 실행하는 데 필요한 자원
- 범위(scope): 검증하려는 특정한 코드 경로(code path)
- 구글에서는 모든 테스트를 크기 기준으로 구분한다.
- 테스트의 크기를 가늠하는 기준은 코드 줄 수가 아니다. 대신 어떻게 동작하고, 무엇을 하고, 얼마나 많은 자원을 소비하는지로 평가한다.
- 쉽게 설명하면, 작은 테스트는 프로세스 하나에서 동작하고, 중간 크기 테스트는 기기 하나에서, 큰 테스트는 자원을 원하는 만큼 사용해 동작한다.
- 구글이 테스트 스위트에 바라는 품질은 속도와 결정성이다.
- 범위에 상관없이 작은 테스트는 더 많은 인프라나 자원을 사용하는 테스트보다 거의 항상 더 빠르고 결정적이다.
- 제약이 가장 엄격하다.
- 테스트가 단 하나의 프로세스에서 실행되어야 한다. 프로그래밍 언어에 따라서는 이 제약을 하나의 스레드까지 좁히는 경우가 많다.
- 서버를 두고 독립된 테스트 프로세스에 연결해 수행하는 방식도 허용되지 않는다. 데이터베이스와 같은 제3의 프로그램을 수행하ㅐ서도 안 된다.
- sleep, I/O 연산 같은 블로킹 호출을 사용해서는 안 된다. 네트워크와 디스크에도 접근할 수 없다는 뜻이다.
- 블로킹 호출을 수반하는 대상을 검사하는 테스트 코드는 테스트 대역을 사용해야 한다. 테스트 대역은 강한 의존성을 가벼운 이프로세스 의존성으로 대체해주는 수단이다.
- 제약들의 목적은 테스트를 느려지게 하거나 비결정적으로 만드는 주요 원인들로부터 작은 테스트를 떼어 놓는 것이다.
- 테스트 케이스가 많은 테스트 스위트에서는 불규칙한 테스트(flaky test)가 단 몇 개만 있어도 원인을 파악하느라 생산성이 급격하게 떨어질 것이다.
- 작은 테스트에 부과된 제약을 그대로 따르기는 어렵지만 유용한 테스트.
- 여러 프로세스와 스레드를 활용할 수 있고, 로컬 호스트로의 네트워크 호출 같은 블로킹 호출도 이용할 수 있다. 단, 외부 시스템과의 통신은 여전히 허용하지 않는다(단 한 대의 기기에서만 수행되어야 한다).
- 데이터베이스 인스턴스를 실행할 수 있게 되어 테스트 대상 코드를 더 현실적인 설정 하에서 검증할 수 있다.
- 유연성이 커지면 반대급부로 테스트는 느려지고 비결정적이 될 가능성이 높아진다. 여러 프로세스에 걸쳐 있거나 블로킹 호출을 하기 시작하면 운영체제나 서드파티 프로세스에 의존하게 된다. 외부 요인이 개입되므로 성능과 결정성을 온전히 우리 스스로가 보장할 수 없다.
- 중간 크기 테스트를 구속하던 로컬 호스트 제약에서 해방되어 테스트와 대상 시스템이 여러 대의 기기를 활용할 수 있게 된다.
- 더 유연해지는 만큼 위험도 늘어난다. 여러 기기에 걸쳐 있느 시스템을 네트워크로 연결해 다루게 되면서 단일 기기에서 구동할 때보다 느려지거나 비결정성이 커질 가능성이 훨씬 높아진다.
- 구글은 큰 테스트를 몇 가지 용도에 한정해서 활용한다.
- 종단간 테스트(end-to-end test), 코드 조각이 아닌 설정을 검증하는 게 주된 목적이다.
- 테스트 대역을 사용하는 게 불가능한 레거시 컴포넌트 테스트
- 큰 테스트는 작은 테스트나 중간 테스트와 분리하여 빌드나 릴리즈 때만 수행되도록 하여 개발 워크플로에 영향을 주지 않도록 한다.
- 테스트 각각이 아주 조금이라도 비결정적이라면 이따금 하나씩 실패하는 일이 드물지 않을 것이다. 실패 확률이 0.1%밖에 안 되더라도 테스트를 하루에 1만 번 수행한다면 매일 평균 10번씩은 실패 원인을 조사해서 조치해야 한다. 팀에 당장 필요한 더 생산적인 일을 할 시간은 그만큼 줄어든다.
- 테스트 결과가 불규칙한 상황이 계속되면 테스트를 믿지 못하는 상황이 올 수 있다. 이 단계에 들어서면 엔지니어들은 테스트가 실패하더라도 더는 신경 쓰지 않는다.
- 경험상 불규칙한 실패가 1%에 이르면 테스트의 가치가 바래기 시작한다.
- 대부분의 불규칙한 실패는 테스트 자체에 비결정적으로 동작하는 로직이 있어서 발생한다. 소프트웨어에는 클록 시간, 스레드 스케줄링, 네트워크 지연시간 등 비결정적인 요소가 많다.
- 모든 테스트는 밀폐(hermetic)되어야 한다. 즉, 셋업, 실행, 테어다운 하는 데 필요한 모든 정보를 담고 있어야 한다.
- 테스트 수행 순서 같은 외부 환경에 관해서는 가능한한 아무것도 가정하지 않아야 한다.
- 테스트는 확인하려는 행위를 수행하는 데 필요한 정보'만' 포함해야 한다.
- 테스트에서는 조건문이나 순환문 같은 제어문을 쓰지 않는 게 좋다. 복잡한 테스트일수록 버그가 숨어들 가능성이 커지며, 실패 원인을 찾기도 어렵다.
- 구글은 테스트 크기를 많이 강조하지만 테스트 범위도 중요하게 생각한다.
- 테스트 범위란 주어진 테스트가 얼마나 많은 코드를 검증하느냐를 말한다.
- 보통 단위 테스트라고 하는 좁은 범위 테스트(narrow-scoped test)는 독립된 클래스느 메서드같이 코드베이스 중 작은 일부 로직을 검증하도록 설계된다.
- 보통 통합 테스트라고 하는 중간 범위 테스트(medium-scoped test)는 적은 수의 컴포넌트들 사이의 상호작용을 검증하도록 설계된다.
- 종단간 테스트, 시스템 테스트 등으로 불리는 넓은 범위 테스트(large-scoped test)는 시스템의 서로 다른 부분들 사이의 상호작용, 혹은 클래스나 메서드 하나만 실행할 때는 괜찮다가 여럿을 조합해 실행하면 나타나는 예기치 못한 동작을 검증하도록 설계된다.
- 일부 다른 테스트 전략에서는 가짜 책체나 모의 객체 같은 테스트 대역을 많이 활용하여 테스트 대상 시스템 바깥 코드가 실행되는 일을 피하기도 하지만, 구글은 딱히 실행할 수 없는 상황이 아니라면 실제 의존성을 끊지 않는 편을 선호한다.
- 구글은 되도록 작고 좁은 테스트를 추구한다. 실제로 구글은 비즈니스 로직 대부분을 검증하는 좁은 범위 단위 테스트가 80%, 둘 이상의 구성요소가 상호작용을 검증하는 중간 범위의 통합 테스트가 15%, 전체 시스템을 검증하는 종단간 테스트가 5% 정도가 되도록 한다.
- 주의해야 할 안티패턴
- 아이스크림 콘(ice cream cone): 종단간 테스트를 많이 작성하고 통합 테스트나 단위 테스트는 훨씬 적게 작성한다. 이런 테스트 스위트는 일반적으로 느리고 신뢰할 수 없으며 고치기도 어렵다.
- 모래시계(hourglass): 종단간 테스트와 단위 테스트는 많지만 통합 테스트가 적다. 아이스크림 콘만큼 나쁘지는 않지만, 중간 범위 테스트였다면 더 빠르고 쉽게 해결했을 문제들을 종단간 테스트로 막아내고 있다. 구성요소들이 강하게 커플링되어 각각의 인스턴스를 독립적으로 만들어낼 수 없을 때 나타난다.
네가 좋아 했다면 테스트를 준비해뒀어야지
- 깨뜨려보고 싶은 모든 것을 테스트하라(성능, 행위 정확성, 접근성, 보안, 시스템이 실패에 대처하는 방법, ...)
- 실패할 때까지 기다리는 대신 흔한 유형의 실패 상황을 시뮬레이션하는 자동 테스트를 작성하자.
- 단위 테스트에서라면 예외나 에러를, 통합 테스트나 종단간 테스트라면 원격 프로시저 호출(RPC) 오류를 주입하거나 지연시간을 늘려볼 수 있다.
- 카오스 엔지니어링 같은 기법을 활용하면 실제 프로덕션 네트워크에 영향을 주는 훨씬 더 큰 중단 사태도 시뮬레이션 할 수 있다.
- 신뢰할 수 있는 시스템이라면 부정적인 조건을 예측하고 대응 방식을 통제할 수 있어야 한다.
- 어느 테스트가 기능 코드의 어느 라인을 실행했는지를 측정하는 수단.
- 테스트 품질을 파악하는 표준 지표로 간주되기도 하지만, 적은 수의 테스트만으로 상당량의 라인을 실행하면서도 의미 있는 동작은 거의 돌려보지 않을 수 있기 때문에 적절치 않다.
- 코드 커버리지는 호출된 라인 수만 셀 뿐, 실행 결과로 어떤 일이 벌어졌는지는 고려하지 않는다.
- 큰 테스트는 커버리지 인플레이션을 일으키므로 커버리지는 작은 테스트에서만 측정하길 권한다.
- 여느 지표와 마찬가지로 커버리지 자체가 목표가 되기 쉽다는 현실이다. 처음에는 매우 합리적으로 들리지만, 목표 커버리지를 달성하면 자발적으로 그 이상을 해야 할 동기가 부족해 그 이상 올리기 어렵다.
- 테스트 스위트의 품질을 높이는 방법은 검사해야 할 행위에 집중하는 것이다.
- 코드 커버리지는 테스트되지 않은 코드가 어디인지는 알려줄 수 있지만, 시스템이 얼마나 제대로 테스트되었느냐를 판가름하는 지표로는 적합하지 않다.
- 코드베이스가 커가다 보면 기존 코드를 변경하는 일을 피할 수 없다. 그런데 자동 테스트가 엉망으로 작성되어 있다면 이럴 때 코드를 변경하기 어렵다.
- 특히 깨지기 쉬운 테스트(brittle test), 즉 예상 결과를 너무 세세하게 표현하거나 광범위하고 복잡한 상용구사 덕지덕지한 테스트가 우리를 가로막는다.
- 깨지기 쉬운 테스트를 만드는 주범으로 모의 객체 오용을 들 수 있다. 모의 객체의 한계를 이해해두면 잘못 사용하는 일이 많이 줄어들 것이다.
- 테스트 스위트가 비결정적이고 느려지면 생산성을 갉아먹는다. 테스트 스위트가 득보다 실이 많다면 엔지니어들은 결국 테스트를 실행하지 않고서라도 구현 작업을 끝마칠 수 있는 방법을 찾으려 할 것이다.
- 거대한 테스트 스위트를 잘 관리하는 비결은 바로 테스트를 존중하는 문화이다. 훌륭한 기능을 출시했을 때와 똑같이 테스트를 견고하게 만든 엔지니어에게 보상해준다.
- 문화를 가꾸는 일과 더불어 린터를 개발하거나 문서자료를 보강하는 등, 안 좋은 테스트를 만드는 실수를 줄여주는 인프라에도 투자해야 한다.
- 지원해야 할 프레임워크와 도구의 수를 줄여서 투자대비 효율을 높인다. 테스트 관리 비용을 낮추는 데 투자하지 않는다면 결국에 엔지니어들이 테스트가 전혀 가치가 없다고 결론내게 될 것이다.
- 자동 테스트를 전사적으로 뿌리내리게 한 원동력
- 오리엔테이션 수업
- 테스트 인증 프로그램
- 화장실에서도 테스트
- 오리엔테이션 때 신규 엔지니어들에게 자동 테스트의 가치를 이야기하는 주제를 배치했다.
- 마치 구글의 관행인 것처럼 소개했다.
- 테스트 인증의 목적은 각 팀이 자신의 테스트 프로세스 수준(성숙도)을 알게 하고 한 단계 올라서기 위한 지침을 제공하는 것이었다.
- 총 5개 레벨로 구성된다.
- 테스트 인증 레벨1: 지속적 빌드 구축, 코드 커버리지 추적, 모든 테스트를 작은/중간 크기/큰 테스트로 구분, 불규칙한 테스트 식별, 바로 실행할 수 있는 빠른 테스트 스위트 마련
- 레벨이 높아질수록 '실패하는 테스트가 없어야 릴리즈 가능', 비결정적 테스트 모두 제거' 같이 더 어려운 조건이 추가된다.
- 레벨5가 되려면 모든 테스트를 자동화하고, 모든 커밋 전에 빠른 테스트 스위트가 수행되도록 하고, 비결정성을 완전히 제거하고, 모든 행위를 테스트해야 한다.
- 2015년에 자동화된 방식(pH)으로 대체될 때까지 테스트 문화 개선에 이바지함.
- 화장실 변기 앞에 전단지를 붙인다.
- 에피소드 하나가 한 페이지를 절대 넘지 않도록 제한하고, 제작자가 가장 중요하고 실행 가능한 조언에 집중할 수 있도록 유도했다.
- 공개 버전
- 신규 입사자 오리엔테이션에서는 여전히 테스트를 가르친다.
- 거의 매주 새로운 TotT가 배포된다.
- 코드를 변경할 때마다 코드 리뷰를 거쳐야 한다. 그리고 모든 변경에는 테스트 코드가 포함되어 있어야 한다.
- 테스트 인증 프로그램을 대체하고자 프로젝트 건실성(Project Health, pH) 개념을 내놓았다. pH는 테스트 커버리지와 테스트 지연시간같이 프로젝트의 건실성을 측정할 수 있는 지표 수십 가지를 지속해서 수집하고 종합하여 하나의 값으로 내어준다. 1이 가장 안 좋고 5가 가장 좋다.
- 테스트를 하라고 강제하지 않았다. 테스트가 훌륭한 아이디어임을 입증하는 데 초점을 맞췄다. 올바른 일이므로 아무도 강요하지 않더라도 계속할 가능성이 크다.
- 모든 종류의 테스트를 다 자동화할 수는 없다. 전화나 영상 통화 시스템의 성능을 평가할 때는 종종 사람에게 판단을 맡긴다.
- 창의력이 필요한 분야에서도 인간이 더 뛰어날 수 있다. 사람이 특정 결점을 발견하고 이해한 다음에는 자동 보안 검사 시스템에 테스트를 추가해 지속해서 수행하고 확장한다.
- 탐색적 테스팅(exploratory testing): 기본적으로 창의력을 요구하는 작업으로, 검사 대상을 마치 고장내야 할 퍼즐로 취급한다.
- 탐색적 테스팅으로 문제를 발견하는 즉시 자동 테스트를 추가하여 문제가 재발하지 않도록 예방해야 한다.
- 이렇게 파악한 문제의 행위들을 자동 테스트에 맡겨놓으면 몸값이 비싼 인간 테스터들은 지루한 반복 작업에서 벗어나 인간이 가장 잘할 수 있는 영역에 집중할 수 있다.
- 자동 테스트는 소프트웨어를 변경할 수 있게 해주는 토대이다.
- 테스트를 확장하려면 반드시 자동화해야 한다.
- 테스트 커버리지를 건실하게 유지하려면 균형 잡힌 테스트 스위트가 필요하다.
- 네가 좋아했다면 테스트를 준비해뒀어야지
- 조직의 테스트 문화를 바꾸는 데는 시간이 걸린다.