10. 단위 테스트의 원칙
1. 단위 테스트 기초
용어
- 테스트 중인 코드 - 실제코드
- 테스트 코드 - 단위 테스트를 구성하는 코드.
- 테스트 케이스
- 준비 arrange - 테스트할 특정 동작 호출전 설정
- 실행 act - 동작을 실제 호출하는 코드
- 단언 assert - 동작의 결과가 올바르게 발생했는지 확인
요즘 대부분의 전문적인 소프트웨어 개발 환경에서는 거의 모든 실제 코드에 단위 테스트가 동반되는 것으로 생각한다.
2. 좋은 단위 테스트는 어떻게 작성할 수 있는가?
좋은 단위테스트가 가져야 할 5가지 주요 기능
- 훼손의 정확한 감지
- 코드가 훼손되면 테스트가 실패한다. 그리고 테스트는 코드가 실제로 훼손된 경우에만 실패해야 한다.
- 세부 구현 사항에 독립적
- 세부 구현 사항을 변경하더라도 테스트 코드는 변경하지 않는 것이 이상적이다.
- 세부 사항에 의존적인 테스트는 리팩토링시에 올바르게 수행했는지 여부와 관계없이 테스트가 실패할 것이다.
- 잘 설명되는 실패
- 코드가 잘못되면 테스트는 실패의 원인과 문제점을 명확하게 설명해야 한다.
- 이해할 수 있는 테스트 코드
- 쉽고 빠르게 실행
3. 퍼블릭 api에 집중하되 중요한 동작은 무시하지 말라
퍼블릭 api에 초점을 맞추면 세부 사항이 아닌 코드 사용자가 궁극적으로 신경 쓸 동작에 집중할 수밖에 없게 된다. 이렇게 하면 실제로 중요한 사항만 테스트하는 데 도움이 되며, 테스트 과정에서 구현 세부 사항에 상관없이 테스트를 수행할 수 있다.
중요한 동작이 퍼블릭 api 외부에 있을 수 있다.
테스트 대상 코드는 수많은 다른 코드에 의존하는 경우가 많은데 의존하는 코드로부터 외부 입력이 제공되거나 테스트 대상 코드가 의존하는 코드에 부수 효과를 일으킨다면 테스트의 의미가 미세하게 달라질 수 있다.
가능하면 퍼블릭 api를 사용하여 코드의 동작을 테스트해야 한다. 이는 순전히 퍼블릭 함수의 매개변수, 반환값, 오류 전달을 통해 발생하는 동작만 테스트해야 한다는 의미다. 그러나 코드의 퍼블릭 api를 어떻게 정의하느냐에 따라 퍼블릭 api만으로는 모든 동작을 테스트할 수 없는 경우가 있다. 몇가지 예시는 다음과 같다.
- 서버와 상호작용하는 코드
- 데이터베이스에 값을 저장하거나 읽는 코드
궁극적으로 중요한 것은 코드의 모든 중요한 동작을 제대로 테스트하는 것이고, 퍼블릭 api라고 생각하는 것만으로는 이것을 할 수 없는 경우가 있다. 테스트 구현 세부 사항에 최대한 독립적으로 수행하도록 주의를 기울여야 하므로 다른 대안이 없는 경우에만 퍼블릭 api를 벗어나 테스트해야 한다.
4. 테스트 더블
단위 테스트는 비교적 격리된 방식으로 코드 단위를 테스트하는 것을 목표로 한다. 하지만 코드는 다른 것들에 의존하는 경우가 있고, 코드의 모든 동작을 완벽하게 테스트하기 위해 종종 입력을 설정하고 부수 효과를 검증해야 한다. 하지만 테스트에서 의존성을 실제로 사용하는 것이 항상 가능하거나 바람직하지만은 않다.
의존성을 실제로 사용하는 것에 대한 대안으로 테스트 더블이 있다. 테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있도록 만들어진다.
테스트 더블을 사용하는 이유
- 테스트 단순화.
- 일부 의존성은 테스트에 사용하기 까다롭고 힘들다.
- 많은 설정이 필요하거나 하위 의존성을 설정해야 할 수 있다.
- 이러면 테스트는 복잡하고 구현 세부 사항과 밀접하게 결합될 수 있다.
- 대신 테스트 더블을 사용하면 단순해진다.
- 테스트로부터 외부 세계 보호
- 일부 의존성을 실제로 부수 효과를 발생한다. (로깅 등)
- 테스트 더블을 사용해 외부 세계에 있는 시스템을 테스트의 동작으로부터 보호할 수 있다.
- 외부로부터 테스트 보호
- 외부 세계는 비결정적일 수 있다.
- 하지만 테스트 더블은 동일하게 결정적 방식으로 작동하도록 설정할 수 있다.
목과 스텁은 문제가 될 수 있다.
이들을 사용할 때 두 가지 주요 단점은 다음과 같다.
- 목이나 스텁이 실제 의존성과 다른 방식으로 동작하도록 설정되면 테스트는 실제적이지 않다.
- 구현 세부 사항과 테스트가 밀접하게 결합하여 리팩토링이 어려워질 수 있다.
목에 대한 의견
- 찬성의견
- 단위 테스트가 더욱 더 격리된다. 목의 사용한다는 것은 테스트가 의존성에 대한 것들을 테스트하지 않는다는 것을 의미한다. 즉, 특정 코드에 문제가 있을 때 해당 코드에 대한 단위 테스트에만 테스트 실패를 유발하며, 이 코드에 의존하는 다른 코드에 대한 테스트는 실패하지 않는다.
- 테스트 코드 작성이 더 쉬워진다. 의존성을 실제로 사용하려면 테스트에 필요한 항목과 해당 의존성을 올바르게 설정하고 확인하는 방법을 파악해야 한다. 반면에 목이나 스텁을 사용하면 실제로 의존성을 설정할 필요가 없고 하위 종속성의 설정에 대해 걱정하지 않아도 되기 때문에 설정이 간단하다.
- 반대의견
- 목은 코드가 특정 호출을 하는지만 확인할 뿐 실제로 호출이 유효한지 검증하지 않는다. 코드의 문제점이 있어도 테스트는 통과할 수 있다.
- 고전적인 접근 방식은 구현 세부 사항에 대해 더 독립적인 테스트를 할 숭 ㅣㅅ다.
5. 테스트 철학으로부터 신중하게 선택하라
테스트 철학과 방법론의 몇가지 예는 다음과 같다.
- 테스트 주도 개발(TDD)
- 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하는 것을 지지한다.
- 실제 코드는 테스트만 통과하도록 최소한으로 작성하고 이후에 구조를 개선하고 중복을 없애기 위해 리팩토링을 한다.
- TDD지지자들은 일반적으로 테스트 케이스를 격리하고 한 테스트 케이스는 하나의 동작만 테스트하도록 집중하며 구현 세부 사항은 테스트하지 않는 등의 여러 다른 모범 사례를 지지한다.
- 행동 주도 개발(BDD)
- 이 철한의 핵심은 사용자, 고객, 비즈니스 관점에서 소프트웨어가 보여야 할 행동을 식별하는 데 집중하는 것이다.
- 이런 원하는 동작은 소프트웨어가 개발될 수 있는 형식으로 포착되고 기록된다. 테스트는 소프트웨어 자체의 속성보다는 이러한 원하는 동작을 반영해야 한다.
- 이런 행동이 정확히 어떻게 포착되고 기록되는지, 어떤 이해관계자가 그 과정에 관여하는지, 그리고 얼마나 공식화되는지는 조직마다 다를 수 있다.
- 수용 테스트 주도 개발(ATDD)
- ATDD는 고객의 관점에서 소프트웨어가 보여줘야 하는 동작을 식별하고 소프트웨어가 전체적으로 필요에 따라 작동하는지 검증하기 위해 자동화된 수락테스트를 만드는 것을 수반한다.
- TDD와 마찬가지로 실제 코드를 구현하기 전에 이러한 테스트를 생성해야 한다. 이론적으로 합격 테스트가 모두 통과하면 소프트웨어는 완전한 것이며 고객이 수락할 준비가 된 것이다.
테스트 철학 및 방법론은 개발자들이 효과적이라고 생각하는 작업 방식을 문서화한다. 하지만 결국 궁극적으로 달성하고자 하는 목표가 그 목표에 도달하기 위해 선택한 작업방식보다 중요하다.
중요한 것은 우리가 좋은 품질의 테스트 코드를 철저하게 작성하고 고품질의 소프트웨어를 생산하는 것이다.