이 글은 소프트웨어 테스트 안티 패턴의 두 번째 파트입니다. 첫 번째 파트를 읽지 않으셨다면 먼저 첫 번째 파트를 읽어주세요.
코드 커버리지는 비즈니스 관리자가 선호하는 지표 중 하나입니다. 그리고 바람직한 코드 커버리지는 개발자와 관리자 사이의 주요 논쟁 거리입니다.
코드 커버리지가 성과 지표로 자주 사용되는 이유는 누구나 이해하기 쉽게 수치로 나타나고, 정확히 측정될 수 있기 때문입니다. 게다가 대부분의 프로그래밍 언어와 테스트 프레임워크에는 쉽게 코드 커버리지를 계산해주는 툴이 존재합니다.
한 가지 사실은 코드 커버리지가 전혀 쓸모 없는 수치라는 것입니다. "정확한" 커버리지 수치란 존재하지 않으며, 이 질문은 누구도 확실하게 대답할 수 없습니다. 100%의 코드 커버리지의 프로젝트에도 여전히 버그는 존재합니다. 대신 관심을 가져야할 수치는 CTM(Codepipes Testing Metrics
- 주: codepipe 블로그에서 생각하는 의미있는 테스트 지표)입니다.
다음은 CTM
의 몇 가지 지표를 정리해놓은 표입니다.
명칭 | 설명 | 이상적인 수치 | 일반적인 수치 | 위험 수치 |
---|---|---|---|---|
PDWT | 테스트를 작성하는 개발자의 비율(%) | 100% | 20 ~ 70% | 100% 미만 |
PBCNT | 새로운 버그에 대해 테스트가 작성되는 비율(%) | 100% | 0 ~ 5% | 100% 미만 |
PTVB | 시스템의 동작(behavior)을 검증하는 테스트의 비율(%) | 100% | 10% | 100% 미만 |
PTD | 예측한 대로 동작하는 (deterministic) 테스트의 비율(%) | 100% | 50 ~ 80% | 100% 미만 |
PDWT
(테스트를 작성하는 개발자의 비율)는 아마 위 지표들 중 가장 중요한 지표일 것입니다. 테스트가 하나도 존재하지 않는 상황에서 테스트 안티 패턴에 대해 말하는 건 앞뒤가 맞지 않으니까요. 팀에 속한 모든 개발자가 테스트를 작성해야 합니다. 새로운 기능을 추가했다면 그에 대응하는 테스트가 작성되어야 합니다.
PBCNT
(새로운 버그에 대해 테스트가 작성되는 비율). 프로덕션에서 발생하는 모든 버그는 새로운 테스트를 작성하기 가장 좋은 이유가 됩니다. 프로덕션에서 발생한 버그는 단 한 번만 발생해야 합니다. 만약 버그를 고친 후에도 비슷한 버그가 계속해서 발생한다면 이 수치를 높이는 게 도움이 될 수 있습니다.
PTVB
(시스템의 동작(behavior)을 검증하는 테스트의 비율). 다섯 번째 안티 패턴에서 이미 언급했듯 강하게 결합된 테스트 코드는 리팩토링을 진행하는데 가장 큰 걸림돌입니다.
PTD
(예측한 대로 동작하는 (deterministic) 테스트의 비율). 테스트는 코드의 비즈니스 로직이 잘못되었을 때에만 실패해야합니다. 뚜렷한 이유 없이 간헐적으로 실패하는 테스트의 문제점은 일곱번째 안티 패턴에서 다룰 예정입니다.
여기까지 왔음에도 정확한 코드 커버리지 수치가 무조건 필요하다고 생각한다면 개인적으로 20%를 추천하고 싶습니다. 이 숫자는 파레토 법칙에 기반한 것으로, 20%의 코드가 80%의 문제를 일으키기 때문입니다. 그러므로 테스트 작성을 시작하는 사람이라면 이 20%의 문제적인 코드에 대한 테스트를 먼저 작성하는 것이 낫습니다. 이 방식은 중요 코드에 대한 테스트를 먼저 작성하라고 조언한 네번째 안티 패턴의 내용과도 일치합니다.
100%의 코드 커버리지를 목표로 삼지 마십시오. 100%의 코드 커버리지는 이론적으로는 완벽하지만 실제로는 시간 낭비일 확률이 높습니다.
모든 상용 애플리케이션에는 복잡한 시나리오를 통해서만 테스트할 수 있는 부분이 존재합니다. 그리고 이런 케이스를 테스트하기 위해 드는 노력은 실제로 해당 케이스가 프로덕션에서 발생할 확률보다 훨씬 큰 경우가 많습니다.
한 번이라도 대규모 애플리케이션 개발에 참여해봤다면 70 ~ 80%의 코드 커버리지에 다다르면 나머지 코드에 대한 테스트를 작성하는 것이 이전보다 훨씬 더 어려워진다는 것을 알 것입니다.
이와 비슷한 논지로 프로덕션에서 절대 실패하지 않는 코드를 테스트하는 것은 그다지 추천하지 않습니다. 그런 코드를 테스트하기 보다는 다른 기능에 대해 테스트를 작성하는 것이 더 낫습니다.
일정 수준의 코드 커버리지를 요구하는 프로젝트는 개발자들로 하여금 사소한 부분까지 테스트하는 코드를 작성하게할 확률이 높습니다. 이는 심각한 시간 낭비이며 만약 관리자가 이런 요구를 해온다면 개발자로서 말도 안 되는 요구에 항의할 수 있어야 합니다.
요약하면 코드 커버리지는 소프트웨어 프로젝트의 질을 나타내는 지표로서 사용되어서는 안 됩니다.
이 부분은 이미 안티 패턴으로서 너무 유명하지만 내용의 완성도를 위해 한 번 더 설명하도록 하겠습니다.
테스트는 버그에 대한 경고 장치로서 사용되기 때문에 항상 믿을 수 있는 결과를 보여줘야합니다. 실패한 테스트는 그 자체로 심각성을 나타내고, 원인이 무엇인지 개발자가 찾아나서게끔 유도해야 합니다.
이런 방식은 테스트가 예측 가능한 방식으로 실패할 때에만 가능합니다. 코드를 바꾸지 않고도 테스트가 실패하거나 통과한다면 이 테스트는 전체 테스트의 신뢰도를 떨어뜨리게 됩니다. 이로 인한 부정적 영향은 다음과 같습니다.
개발자가 테스트를 더 이상 신뢰하지 않게 되어 그 결과를 무시하게 됩니다.
설령 제대로 된 테스트 몇 개가 실패하더라도 대부분의 테스트가 예측 불가능한 방식으로 실패한다면 그 두 개를 구분하기는 쉽지 않습니다.
실패하는 테스트는 누구나 명확하게 인지할 수 있어야합니다. 반대의 경우 실패 원인을 제대로 파악하기가 어렵습니다.
<새로운 기능을 추가했을 때 실패의 원인이 명확한 경우에는 새로운 기능의 추가가 원인임을 쉽게 파악할 수 있지만, 그렇지 않은 경우 (아래)에는 원인 파악이 쉽지 않다>
이와 비슷하게 테스트 실행 속도가 너무 느리다면 빠른 피드백을 원하는 개발자가 결과를 무시하거나 아예 실행시키지도 않게 될 것입니다.
현실에서 예측하기 어렵거나 느린 테스트는 거의 통합 테스트나 엔드 투 엔드 테스트입니다. 테스트 피라미드의 상층부로 올라갈 수록 예측하기 어려운 테스트의 비율이 커집니다. 브라우저와 상호작용하는 테스트는 제대로 작성하기가 정말 어렵죠. 통합 테스트가 예측할 수 없게 되는데는 많은 이유가 있지만 일반적으로는 테스트 환경 구축의 문제인 경우가 많습니다.
이런 테스트에 대한 최선의 해결책은 테스트를 다른 테스트로부터 분리시켜 놓는 것입니다. (이런 테스트를 쉽게 고칠 수 없다는 가정하에 말이죠) 이번 패턴에 대한 해결책은 웹 상에서 쉽게 찾을 수 있기 때문에 더 자세하게 언급하지는 않겠습니다.
요약하면, 테스트는 개발자에게 신뢰감을 주어야 합니다. 실패하는 테스트는 그 자체로 개발자에게 경종을 울릴 수 있어야하며, 절대로 프로덕션에 배포될 수 없다는 인상을 주어야 합니다.
팀에 따라 서로 다른 테스트를 갖추고 있을 것입니다. 단위 테스트, 부하 테스트(load tests
), 사용자 인수 테스트(user acceptance tests
) 같은 것들 말이죠.
이상적으로 모든 테스트는 별도의 조작 없이 자동으로 실행되어야 합니다. 그게 여의치 않다면 적어도 단위 테스트와 통합 테스트는 자동으로 실행되어야 합니다. 그렇게 해야 다른 기능으로 넘어가기 전에 적절한 피드백을 받고 빠르게 코드를 고칠 수 있기 때문입니다.
역사적으로 소프트웨어 생애주기에서 가장 느린 과정은 애플리케이션의 배포였습니다. 하지만 점진적인 클라우드 환경으로의 이전으로 새로운 머신을 생성하는 시간이 수 초에서 수 분 단위로 줄어들었습니다. 이러한 급격한 패러다임 전환은 대부분의 조직에 큰 충격으로 다가왔습니다. 이전에 확립된 배포 프로세스 베스트 프렉티스(best practice
)가 일정한 간격을 두고 배포하는 모델에 초점이 맞춰져 있었기 때문이죠. 이제 가능한 한 빠른 배포를 목표로 한다면 QA를 통과하기 위해 매뉴얼하게 기능을 점검하는 절차는 가장 먼저 사라져야합니다.
빠른 배포는 각각의 배포 결과를 믿을 수 있다는 점을 전제로 합니다. 자신감을 얻는 방법에는 여러 가지가 있겠지만 가장 첫 번째 단계는 소프트웨어 테스트를 갖추는 것입니다. 그리고 테스트를 통해 버그를 잡는 것 못지않게 테스트 실행을 자동화하는 것도 중요합니다.
많은 회사가 지속적 통합/배포(CI/CD)를 실천하고 있다고 생각합니다. 하지만 진정한 지속적 통합/배포는 어떤 코드 버전(주: 깃 등의 버전 관리 시스템으로 관리되는 브랜치 등)이라도 항상 배포될 준비가 되어 있다는 것을 의미합니다. 물론 해당 코드 버전은 이미 테스트를 마친 상태여야 하겠죠. 그러므로 배포를 할 때마다 수동으로 QA를 거쳐야 한다면 진정한 지속적 통합/배포를 실천한다고 말하기 어려울 것입니다.
그러나 많은 회사가 배포가 자동화되어야한다는 점을 알면서도 여전히 반쯤 수동적인 테스트 프로세스를 따릅니다. 여기서 반쯤 수동적이라는 것은 테스트 환경을 준비하거나 테스트 데이터를 정리하는 과정에서 여전히 사람 손이 필요한 것을 말합니다. 하지만 이는 진정한 테스트 자동화라고 할 수 없습니다. 테스트와 관련된 모든 행위는 자동화되어야만 합니다.
가상머신이나 컨테이너를 사용하는 방법은 손쉽게 필요에 따라 테스트 환경을 구축하는 좋은 방법입니다. 그러므로 각 풀 리퀘스트마다 자동으로(on the fly) 테스트 환경을 구축하는 방식은 조직의 표준적인 테스트 실행 방식이 되어야 합니다. 이제 각 기능은 독립적으로 테스트되므로, 테스트에 실패한 기능이 다른 기능의 배포를 막어서서는 안 됩니다.
어떤 회사의 테스트 자동화 수준을 살펴보는 좋은 방법은 QA/테스트 엔지니어의 업무를 살펴보는 것입니다. 이상적으로 테스트 엔지니어는 새로운 테스트 케이스를 작성하는 일만 해야 합니다. 테스트는 빌드 서버에 의해 실행되며 엔지니어는 테스트를 수동으로 실행시키지 않습니다.
테스트는 의식적으로 실행되어야 하는 존재가 아닌 빌드 서버에 의해 자동으로 실행되는 존재가 되어야 합니다. 적어도 5 ~ 15분 안에는 개발자가 테스트 실행 결과를 받아볼 수 있어야 하며, 테스트 엔지니어는 수동으로 테스트를 실행하는 대신 새로운 테스트를 작성하고 기존 테스트를 리팩토링하는 일에만 집중해야 합니다.
(주: 이등 시민, 일반적으로 법적/제도적으로 차별받는 대상을 가리키며, 테스트 코드를 깨끗한 코드 원칙을 지키지 않아도 되는 예외라고 생각하는 경향을 말함)
숙련된 개발자라면 코드를 구현하기 전에 어느 정도 구조를 잡는데 시간을 쓸 것입니다. 몇 가지 디자인 원칙은 너무 유명해서 위키피디아 페이지에서도 확인할 수 있습니다.
첫번째 원칙은 코드를 중복하지 말라는 원칙입니다. 프로그래밍 언어에 따라 다양한 베스트 프렉티스와 디자인 패턴이 존재할 수 있고, 팀에만 존재하는 특별한 가이드라인도 있을 수 있습니다.
그럼에도 몇몇 개발자는 이러한 원칙을 적용하지 않습니다. 결국 기능 코드는 잘 정리되어 있지만 테스트 코드에 심각한 코드 중복, 하드코딩(주: 변수 대신 리터럴 값을 사용하는 경우)된 변수, 복사-붙여넣기와 같은 증상 겪게됩니다.
테스트 코드를 이등 시민으로 대하는 경향은 모든 코드가 유지보수되어야 한다는 점을 고려하면 그다지 설득력이 없습니다. 테스트 코드 또한 언젠가 수정되고 리팩토링되어야 합니다. 변수와 구조도 바뀌어야 합니다. 테스트를 어떻게 디자인할 것인가를 고려하지 않는다면 스스로 기술 부채를 쌓는 것과 크게 다르지 않습니다.
기능에 대한 코드를 작성하는 것처럼 테스트 코드를 작성할 때도 관심을 가져야 합니다.
정적 분석기나 코드 포매팅/퀄리티 툴을 사용하고 있다면 테스트 코드를 작성할 때도 똑같이 사용하십시오.
요약하면 테스트 코드 또한 프로덕션 코드와 마찬가지로 세심하게 작성되어야 한다는 것입니다.
테스트를 하는 목적 중 하나는 버그를 잡는 것입니다. 네 번째 안티 패턴에서 살펴봤듯, 대부분의 애플리케이션에는 많은 버그가 발생하는 핵심 코드 영역이 존재합니다. 버그를 고치면 해당 버그가 다시 발생하지 않게끔 해야 합니다. 이를 강제하는 가장 좋은 방법은 버그를 수정하면서 같이 테스트를 작성하는 것입니다.
프로덕션 환경에서 발생한 버그는 테스트를 작성하기 좋은 대상이 됩니다. 그 이유는
프로덕션에서 발생한 버그에 대한 테스트 코드를 작성하지 않는 팀들도 있습니다. 그들은 코드를 고치고선 버그를 수정했다고 말합니다. 그리고 어떤 이유에서인지는 몰라도 다수의 개발자들이 테스트는 새 기능을 추가할 때만 가치있다고 생각합니다.
저는 이 견해에서 벗어나 반대로 새 기능을 추가할 때 작성하는 테스트보다 실제로 발생한 버그에 대한 테스트가 더 중요하다고 말하고 싶습니다. 결국 새로운 기능이 프로덕션 환경에서 얼마나 버그를 발생시킬지는 알 수 없기 때문입니다. (어쩌면 해당 코드는 핵심 영역의 코드가 아니어서 자주 버그가 발생하지 않을 수도 있죠.)
반면 실제 버그에 대한 테스트는 굉장히 가치있습니다. 내 수정 사항이 실제로 버그를 고쳤는지 증명해줄만 아니라 이후에 리팩토링을 하더라도 내 수정된 코드가 작동할 것을 입증하기 때문입니다.
테스트 코드가 존재하지 않는 레거시 프로젝트에 참여할 때 테스트 코드를 작성함으로써 가장 확실한 결과를 볼 수 있는 방법은 실제 버그에 대한 테스트를 작성하는 것입니다. 어떤 코드가 테스트가 필요한지 짐작하는 대신 실제 버그에 관심을 가지고 그 부분을 테스트를 통해 커버하려고 노력해야 합니다. 시간이 지나면 테스트 코드가 핵심 영역을 커버할 수 있게 될 것이고, 더 나아가 자주 버그가 발생하는 영역 모두가 테스트 코드에 의해 커버될 것입니다.
버그가 발생했을 때 테스트 코드를 작성하지 않아도 되는 경우는 오로지 그 버그가 코드에 의해 발생하지 않았을 때 뿐입니다. 잘못된 로드 밸런서 설정은 유닛 테스트를 통해 해결될 수 있는 문제는 아니니까요.
요약하면 어떤 부분에 대한 테스트를 작성할 지 알 수 없을 때 프로덕션에 존재하는 버그는 좋은 테스트 작성 대상이 된다는 것입니다.
TDD는 테스트 주도 개발을 말합니다. TDD 또한 다른 모든 방법론과 마찬가지로 좋은 아이디어지만 컨설턴트가 어떤 회사에 TDD를 맹목적으로 지키는 것만이 상황을 해결할 수 있는 유일한 방법이라는 식으로 얘기하기 시작하면 더 이상 좋다고 말할 수 없습니다.
넓게 보면 테스트는
TDD의 핵심 원칙 중 하나는 언제나 구현을 하기 전에 테스트를 작성하라는 1번 원칙을 따르라는 것입니다. 구현을 하기 전에 테스트를 작성하는 건 좋은 방법이지만 언제나 최고의 방법은 아닙니다.
구현을 하기 전에 테스트를 작성한다는 것에는 최종 API가 어떤 형태일지 안다는 가정에서 출발합니다. 물론 명확한 명세가 이미 나와있어서 함수의 형태를 미리 알고 있을 수도 있습니다. 하지만 어떤 경우에는 그냥 한 번 코드를 적어보고 싶을 수도 있고 해답을 찾기 전에 이리저리 실험해볼 수도 있습니다.
좀 더 현실적인 조언은 스타트업에서 TDD를 맹목적으로 따르는 것이 맞지 않을 수 있다는 것입니다. 스타트업의 코드는 언제나 빠르게 변하기 때문에 TDD가 큰 도움이 되지 못할 것입니다. 어떤 경우에는 올바른 구현을 하기 위해 이전의 코드를 삭제해야 할 때도 있습니다. 이 경우에는 코드를 작성한 후에 테스트를 작성하는 것이 더 나은 전략일 수 있습니다.
테스트를 전혀 작성하지 않는 것 또한 한 가지 전략이 될 수 있습니다. 네 번째 안티 패턴에서 보았듯 어떤 코드 영역은 테스트가 불필요합니다. 사소한 코드에 대해서조차 TDD에서 강제하기 때문에 테스트를 작성해야 한다면 그 어떤 도움도 되지 못할 것입니다.
어떤 상황에서라도 테스트를 먼저 작성해야한다는 TDD 열성론자들의 주장 때문에 수 많은 개발자들이 정신적인 고통을 겪어야만 했습니다. TDD에 대한 병적인 집착은 이미 다른 많은 곳에서 언급된 바 있습니다.
이쯤에서 개인적으로 이런 방식으로 코드를 짠 적이 있다는 것을 고백해야 겠습니다.
한 마디로 TDD는 좋은 방법이지만 언제나 따를 필요는 없다는 것입니다. 당신이 포츈지 500에 실리는 큰 기업에서 일하고, 비즈니스 애널리스트가 건네주는 명확한 구현 명세를 확인할 수 있다면 TDD가 분명 큰 도움이 될 것입니다.
반대로 새로운 프레임워크를 실험해보거나 동작방식을 파악하고 싶다면 굳이 TDD를 따르지 않아도 무방합니다.
직업 개발자는 어떤 도구라도 트레이드 오프가 있다는 것을 이해하고 있는 사람입니다. 어떤 기술을 사용하기 위해서는 프로젝트 초반에 시간을 들여서 공부해야할 수도 있습니다. 언제나 새로운 웹 프레임워크가 등장하고, 프레임워크를 잘 이해하면 효율적이고 깔끔한 코드를 작성하는데 큰 도움이 됩니다.
테스트에 대한 관점 또한 마찬기지입니다. 어떤 개발자는 테스트를 1순위에 두지 않기 때문에 테스트 프레임워크가 어떤 기능을 제공하는지 눈여겨 보지 않습니다. 다른 프로젝트에서 코드를 복사해오거나 하면 겉으로 보기에는 잘 돌아가는 것처럼 보일 수 있겠지만 이는 직업 개발자의 자세로 볼 수 없습니다.
이런 일은 자주 발생합니다. 수많은 사람들이 이미 테스트 프레임워크에서 지원하는 기능인지 알지 못한 채 수많은 헬퍼 함수를 작성하고 있으니까요.
이들은 테스트를 이해하기 힘들게 만들며 저는 이 때문에 몇 번이나 "영리한 헬퍼 함수"(clever helpers)를 라이브러리에 명시된 표준 함수로 바꾸는 작업을 해야만 했습니다.
좋은 테스트를 작성하려면 테스트 프레임워크가 어떤 API를 제공하는지 알고 있어야 합니다. 아래와 같은 것들부터 배워보는 것도 좋습니다.
parameterized tests
)teardown
)categorization
)conditional running
)웹 애플리케이션에 대한 테스트를 작성하려고 한다면 적어도 아래 것들에 대해서 베스트 프렉티스가 무엇인지 정도는 알고 있어야 합니다.
http
클라이언트 라이브러리http
목킹 서버mutation/fuzz testing
)clean up/rollback
)이미 솔루션이 존재하는 영역을 다시 구현할 필요는 없습니다. 어떤 경우에는 직접 헬퍼 함수를 만들어야만 하는 경우도 있겠지만 대부분 굳이 그렇게 할 필요가 없습니다.
이 패턴은 가장 마지막에 있긴 하지만 제가 이 글을 쓰게 된 가장 큰 이유 중 하나입니다. 저는 컨퍼런스나 미팅에서 "내 애플리케이션은 테스트를 작성하지 않아도 잘 동작하고 있어"라는 식으로 말하는 사람들을 볼 때마다 실망을 감출 수 없습니다. 물론 좀 더 일반적인 유형은 특정 테스트를 쓸모 없는 것으로 치부하는 사람들이겠지만요. (단위 테스트나 통합 테스트가 필요없다고 얘기하는 사람들)
이런 사람들을 볼 때마다 저는 왜 이들이 그렇게 말하는지 살펴봅니다. 그리고 언제나 그 끝엔 안티 패턴이 발견됩니다. 어떤 회사에서는 테스트가 너무 느렸고 (일곱 번째 안티 패턴), 지속적인 리팩토링이 필요하거나 (다섯 번째 안티 패턴) 또는 정당한 이유 없이 100%의 코드 커버리지를 강요당하기도 했으며 (여섯 번째 안티 패턴) TDD 신봉론자들에게 고통을 받기도 했죠. (열한 번째 안티 패턴)
저는 이 상황을 충분히 이해합니다. 안티 패턴을 따르며 근무하는 일이 얼마나 어려운지 알고 있습니다.
그럼에도 새로운 프로젝트를 시작할 때 과거의 나쁜 경험이 테스트를 주저하게 만드는 원인이 되어서는 안 됩니다. 프로젝트를 돌아보고 어떤 안티 패턴이 있었는지 떠올려보세요. 만약 어느 하나라도 해당된다면 단지 나쁜 방식으로 테스트하고 있었을 뿐입니다. 이런 경우에는 테스트가 많은 적든 애플리케이션 퀄리티에는 큰 도움이 되지 않습니다.
하지만 잘못된 테스트 환경에 고통받는 것과 주니어 개발자에게 테스트를 작성하는 건 시간낭비라고 말하는 것은 다른 이야기 입니다. 제발 그러지 마십시오. 대신 어떤 안티 패턴에도 해당되지 않는 테스트 환경을 목표로 삼으십시오.
오역에 대한 피드백 및 질문 환영합니다. 긴 글 읽어주셔서 감사합니다.
번역 정말 감사합니다!! 잘읽었습니다!!