💡 "단위 테스트"의 "단위"라는 용어는 원래 테스트 중인 시스템의 단위가 아니라 테스트 자체를 지칭한다는 말이 있습니다. 이는 테스트가 하나의 단위로 실행될 수 있고 이 테스트 이전에 실행되는 다른 테스트에는 의존하지 않는다는 것을 의미합니다 (이곳과 이곳을 참고하세요). 또 다른 모순된 관점은 다음과 같습니다. "테스트할 단위는 전반적으로 혼란과 논쟁의 여지가 있습니다. 단위가 실제 행위일 때 Martin은 "객체 지향 설계는 클래스를 단위로 취급하는 경향이 있으며 절차적 또는 기능적 접근 방식은 하나의 기능을 단위로 취급하곤 한다."고 지적했습니다."
그러나 사람들이 "단위"를 시스템의 클래스 또는 메서드로 간주할 때 일반적으로 두 가지 일이 일어납니다. 첫 번째는 개발자가 모든 클래스나 메서드에 대해 하나의 "단위 테스트"를 독단적으로 작성한다는 것입니다. 두 번째는 테스트 더블 (mock)을 사용해 이 "단위"들을 다른 "단위"들로부터 분리하는 것입니다.
이제 코드 베이스에서 약간의 변경 사항이 생기면 테스트 스위트에서 알려주는 단 한 가지는 하루 종일 가짜 양성 테스트 사례를 다시 작성하느라 바빠진다는 것뿐입니다.
단위를 서로 분리해야 한다는 주장은 숨어있는 버그를 더 쉽게 발견할 수 있기 때문이라고 합니다. 테스트 스위트가 어떤 클래스·메서드·함수에서 문제가 발생했는지 정확하게 알려줄 것이라는 생각이죠. 하지만 저는 이후 방대한 양의 가짜 양성 테스트 사례가 발생하고 이 문제를 해결하는 데 필요한 시간을 고려한다면 이 방법은 효과적이지 않다고 생각합니다. 또한 코드 베이스에 대한 약간의 지식이 있다면 문제가 어디서 발생했는지 알 수 있을 것입니다. 그렇지 않다면 이번 기회에 코드 베이스를 더 잘 알게 될 수 있겠군요.
안타깝게도 단위 테스트의 현재 인식이 매우 경직되어 있어 자유로운 사고가 거의 불가능합니다. 하지만 테스트에 관해 실제로 두 가지 학파인 모의 객체 테스트 주의 vs 고전 테스트 주의가 있다는 것을 알면 조금 더 유연하게 생각하는 데에 도움이 될 것입니다. 아래의 팁은 주로 고전 테스트 주의에서 영감을 받은 "좋은" 테스트를 작성하는 방법에 대한 내용입니다. 다만 소프트웨어 엔지니어링에는 좋고 나쁨은 없으며 단지 그것이 당신의 요구 사항을 충족시키는가만이 중요하다는 것을 명심하세요.
팁 #1: 외부에서 내부로 테스트를 작성하세요. 즉, 현실적인 사용자 관점에서 테스트를 작성해야 한다는 뜻입니다. 최고의 품질을 보증하고 리팩토링에 대한 내성을 갖기 위해 우리는 보통 e2e 또는 통합 테스트를 작성합니다. 하지만 이러한 경우 테스트를 실행하는 데 시간이 오래 걸리고 피드백 루프가 증가할 수 있습니다. 이 문제는 테스트를 서로 독립적으로 만들어 병렬적으로 실행할 수 있도록 함으로써 해결할 수 있습니다. 원래 테스트 피라미드는 많은 종단 간 테스트 및 통합 테스트를 금지했습니다. 대신 많은 단위 테스트를 작성하도록 했으며 대부분 사람들은 단위를 클래스로 여겼습니다. 그리고 이런 현상은 시스템의 행위보다 시스템의 구조를 테스트하는 내부 접근 방식으로 이어지는 경우가 많습니다. 기존의 테스트 피라미드 기조에 맞서 종단 간 통합 테스트와 단위 테스트가 당신의 상황에 얼마나 적합한지 생각해 보세요. 또한 테스트 피라미드에 대한 최근의 대안인 "Honeycomb" 및 "The Testing Trophy"도 고려해 보시기 바랍니다.
팁 #2: 테스트할 때 코드를 분리하지 마세요. 그럴 경우 테스트가 취약해지며 소프트웨어를 리팩토링하는 경우에도 도움이 되지 않습니다. 실제 외부 서비스로부터만 코드를 격리하세요. 인프라 코드에서 "기본 코드"를 분리하는 데 좋은 출발점이 되는 포트 및 어댑터 패턴 (일명 육각형 아키텍처)을 살펴보세요. 스텁할 때는 인프라의 구현을 스텁하세요. 또한 인프라를 스텁하지 않고 실제 데이터베이스를 사용하는 방안도 고려해 보세요. Docker와 같은 도구를 사용하면 그렇게 어렵거나 느리다고 느껴지지 않을 겁니다. 그리고 테스트 중인 "단위"를 더 많이 분리할수록 테스트 커버리지의 기록이 무의미해 집니다. 각 라인을 테스트하더라도 시스템이 전체적으로 작동하는지는 알 수 없게 된다는 뜻입니다. 그리고 이런 부분은 동적 유형 언어의 경우에 특히 더 해당합니다.
팁 #3: 적절한 TDD를 수행하려면 빨간색 테스트 없이 코드를 변경해서는 안 됩니다. 여기에는 두 가지 이점이 있습니다. 1) 테스트 자체에 대한 테스트라는 점입니다. 테스트가 빨간색이면 제대로 작동한다는 것을 알 수 있습니다. 2) 모든 시나리오를 테스트했는지 확인할 수 있다는 점입니다. 물론 이 부분은 코드를 리팩토링할 때는 적용되지 않습니다. 때로는 코드를 먼저 작성하고 테스트는 나중에 작성하는 데 어려움을 겪기도 합니다. 그러면 의도적으로 버그를 발생시켜 테스트 스위트가 빨간색으로 바뀌는지 확인하게 되죠. 이 두 번째 사항에 대해서는 테스트 커버리지 기록을 통해 테스트 되지 않은 코드를 사용했는지 확인할 수 있습니다. 소프트웨어의 공개 인터페이스로 테스트 되지 않은 라인에 접근할 수 없다면 해당 라인을 삭제해도 괜찮습니다. 테스트 커버리지 100%를 달성하기 위해 stub 또는 mock을 사용하기보다 현실적인 관점에서 API를 사용해 이러한 분기를 어떻게 실행할지에 대한 시나리오를 찾으세요. 그럴 경우 커버리지 기록이 다시 유용해질 것이며 높은 커버리지를 달성하는 것은 실제로도 중요한 문제입니다.
팁 #4: TDD에서는 테스트를 작성하는 절차가 소프트웨어 설계를 주도할 것이고 주도해야 한다고 합니다. 하지만 저는 이 부분에 절대 동의하지 않습니다. 다른 사람들에게는 효과가 있을지 모르지만 저에게는 그렇지 않습니다. 소프트웨어 아키텍처 101에서는 비기능적 요구사항(NFR)이 아키텍처를 정의한다고 말합니다 (이 주제에 대해 실제로 제가 가장 좋아하는 책인 Bass, Clements, Kazman의 저자의 "Software Architecture in Practice"라는 훌륭한 책을 추천합니다). NFR은 일반적으로 단위 테스트를 작성할 때 역할을 수행하지 않습니다.
요약하자면 저는 자동화된 테스트를 작성할 때 무엇을 절충할지 결정하는 것이 가장 중요하다고 생각합니다. 높은 수준의 품질 보증, 리팩토링에 대한 내성, 아니면 빠른 피드백 루프를 원하시나요? 오늘날에는 e2e 테스트 또는 통합 테스트를 매우 빠르게 실행할 수 있는 경우가 많습니다.
"테스트가 소프트웨어 사용 방식과 덜 유사할수록 테스트를 통해 얻을 수 있는 신뢰도는 낮아집니다." —https://twitter.com/kentcdodds/status/977018512689455106?s=20