단위 테스트 vs 통합 테스트 차이
단위 테스트(Unit Test)
단위 테스트(Unit Test)는 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트이다. 여기서 모듈은 애플리케이션에서 작동하는 하나의 기능 또는 메서드로 이해할 수 있다. 예를 들어 웹 애플리케이션에서 로그인 메서드에 대한 독립적인 테스트가 1개의 단위테스트가 될 수 있다.
즉, 단위 테스트는 애플리케이션을 구성하는 하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것으로, "어떤 기능이 실행되면 어떤 결과가 나온다" 정도로 테스트를 진행한다.
통합 테스트(Integration Test)
통합 테스트(Integration Test)는 모듈을 통합하는 과정에서 모듈 간의 호환성을 확인하기 위해 수행되는 테스트이다.
일반적으로 애플리케이션은 여러 개의 모듈들로 구성이 되고, 모듈들끼리 메시지를 주고받으면서(함수 호출) 기능을 수행한다. 그렇기에 통합된 모듈들이 올바르게 연계되어 동작하는지 검증이 필요한데, 이러한 목적으로 진행되는 테스트가 통합 테스트이다. 그렇기에 통합 테스트는 독립적인 기능에 대한 테스트가 아니라 웹 페이지로부터 API를 호출하여 올바르게 동작하는 지를 확인하는 것이다.
단위 테스트(Unit Test) 작성의 필요성
일반적으로 실무에서 테스트 코드를 작성한다고 하면 거의 단위 테스트를 의미한다. 통합 테스트는 실제 여러 컴포넌트들 간의 상호작용을 테스트하기 때문에 모든 컴포넌트들이 구동된 상태에서 테스트를 하게 된다. 그렇기에 통합 테스트를 위해서는 캐시나 데이터베이스 등 다른 컴포넌트들과 실제 연결을 해야 하고, 시스템을 구성하는 컴포넌트들이 많아질수록 테스트를 위한 비용(시간)이 상당히 커진다. 반면에 단위 테스트는 해당 부분만 독립적으로 테스트하기 때문에 어떤 코드를 리팩토링하여도 빠르게 문제 여부를 확인할 수 있다.
- 테스팅에 대한 시간과 비용을 절감할 수 있다.
- 새로운 기능 추가 시에 수시로 빠르게 테스트 할 수 있다.
- 리팩토링 시에 안정성을 확보할 수 있다.
- 코드에 대한 문서가 될 수 있다.
그렇기 때문에 실무에서는 단위 테스트를 선호하며, 요즘 많이 사용되는 TDD(Test-Driven Development, 테스트 주도 개발)에서 얘기하는 테스트도 단위 테스트를 의미한다. 우리는 우리가 작성한 테스트 코드를 수시로 빠르게 돌리면서 문제를 파악할 수 있다.
단위 테스트의 세 가지 속성
단위 테스트는 크게 3가지 정의로 볼 수 있다.
1. 작은 코드 조각(단위)을 검증한다.
2. 빠르게 수행한다.
3. 격리된 방식으로 처리하는 자동화된 테스트다.
고전파, 런던파는 1,2번의 정의는 동일하나 3번, 테스트를 할 때 격리를 바라보는 관점이 다르다. 어떤 차이가 있을까?
런던파의 테스트 격리
런던파에서 격리 문제는 테스트 대상 시스템(System under test, sut)을 협력자(coolaborator)에게서 격리하는 것을 말한다. 예를 들어 A 클래스를 테스트한다고 했을 때 A 클래스가 B나 C 클래스에 의존한다면 이 모든 의존성을 테스트 대역(Test Double)으로 대체한다. A 클래스 동작을 외부 영향과 격리(분리)해서 테스트 대상 클래스에만 집중할 수 있도록 하는 것이다.
위의 테스트는 SUT가 1,2번에 의존적이었지만 런던파 테스트로 한다면 의존성을 테스트 대역으로 대체하여 격리된 환경에서 테스트할 수 있다.
장점
- 테스트가 실패하면 의존성이 없기 때문에 테스트 대상의 문제만이라는 것을 알 수 있다. -> 쉽게 문제점을 발견할 수 있다.
- 객체 그래프(object graph)를 분할할 수 있다. 의존성을 중간에 끊을 수 있기 때문에 유일하게 해당 클래스만 테스트할 수 있다.
- 프로젝트에서 한 번에 한 클래스만 테스트하라는 지침이 있다면, 전체 단위 테스트 스위트를 간단한 구조로 만들 수 있다.(아래 그림)
고전파의 테스트 격리
고전파의 격리하는 방법은 코드를 격리하는 것을 넘어 단위 테스트 간에 격리한다. 테스트 순서와 상관없이 각각의 단위 테스트는 서로의 결과에 영향을 미치지 않는다. 각각의 테스트를 격리하는 것은 각각의 클래스들 간에 멤버 변수를 공유하거나 영향을 미치지 않는다면 한 번에 테스트해도 된다.
단위 테스트 간에 격리시키기 어려운 상황이 있다. 테스트할 때 공유 의존성(shared dependency)을 사용하는 경우다.
공유 의존성이란 테스트 간에 공유되고 서로 결과에 영향을 미칠 수 있는 의존성이다. 예를 들어 static 멤버 변수가 있다고 했을 때 테스트 간에 static 멤버 변수를 수정하면 다음 테스트에도 영향을 미친다. 또는 데이터 베이스나 파일 시스템을 다룰 때도 공유 의존성이 있다고 볼 수 있다. 따라서 고전파에서 공유 의존성을 없애고 테스트해야 한다.
정리
런던파와 고전파의 차이는 단위 테스트를 할 때 격리의 차이에서 발생한다. 런던파는 sut(클래스) 간 격리하고 고전파는 단위 테스트 간 격리한다.
격리 주체 | 단위 크기 | 테스트 대역 사용 대상 | |
런던파 | 단위 | 단일 클래스 | 불변 의존성 외의 모든 의존성 |
고전파 | 단위 테스트 | 단일 클래스 또는 클래스 세트 | 공유 의존성 |
런던파와 고전파의 장단점과 차이
런던파의 장점과 반박
장점 1 : 한 번에 한 클래스를, 자세한 메서드, 클래스 확인하기 때문에 테스트 범위가 좁고 세밀하다.
반박 1 : 테스트 목표가 입자성이 아니다. 단일 동작 단위가 좋은 테스트이지 않을까? 예를 들어 '우리 집 강아지를 부르면, 바로 나에게 온다'의 동작 테스트가 중요한 것이지 '우리 집 강아지를 부르면 먼저 왼쪽 앞다리를 운직이고, 이어서 오른쪽 앞다리를 움직이고...의 테스트는 좋지 못하다.
장점 2 : 클래스의 그래프가 커져도(연결된 의존성이 많아져도) 테스트하기 쉽다. 테스트 대역으로 대체하면 된다.
반박 2 : 클래스 그래프가 커진 것을 테스트 대역으로 테스트할 방법을 찾는 게 중요한 것이 아니라 애초에 큰 클래스 그래프 설계가 잘못된 것이지 않을까?
장점 3 : 테스트 내 다른 의존성이 없기 때문에 테스트 실패한다면 SUT에만 문제가 있기 때문에 문제 파악이 쉽다.
반박 3 : 오히려 의존성이 연결된 모든 곳까지 테스트하기 때문에 그 문제를 고치면서 의존성의 구현체까지 고칠 수 있다. 계단식으로 실패하는 데 가치가 있다.
TDD 설계 방식 차이
런던파 : 하향식(탑다운), 상위 레벨 테스트부터 시작하고 하위는 대역으로 대체하여 나중에 구현할 수 있다.
고전파 : 테스트에서 실제 객체를 다뤄야 하기 때문에 일반적으로 상향식(바텀업)으로 구현한다. 도메인 모델을 시작으로 구현하는 편이다.
테스트 코드와 SUT의 결합도 차이
런던파 테스트 코드는 SUT의 코드, 클래스 단위로 테스트하다 보니 고전파보다 테스트 코드가 SUT와 더 강하게 결합되는 편이다. 이는 운영 코드가 바뀌면 테스트 코드도 다 바뀌어야 하는 문제가 있다. 런던파 테스트 코드는 깨지기 쉽다(Fragile Tests)
오버 엔지니어링
고전파의 경우 바텀업, 저수준부터 개발하기 때문에 나중에 필요치 않은 코드, 오버 엔지니어링 될 수 있다.
단위 테스트의 문제점과 Stub
어떤 객체가 자체적으로 모든 일을 처리한다면 문제가 없겠지만, 일반적인 애플리케이션에서는 1개의 기능을 처리하기 위해 다른 객체들과 메세지를 주고받아야 한다. 하지만 앞서 설명하였듯 단위 테스트는 해당 모듈에 대한 독립적인 테스트이기 때문에 다른 객체와 메시지를 주고받는 경우에 문제가 발생한다. 그렇기 때문에 다른 객체 대신에 가짜 객체(Mock Object)를 주입하여 어떤 결과를 반환하라고 정해진 답변을 준비시켜야 하는데, 이를 stub이라고 한다.
예를 들어 데이터베이스에 새로운 데이터를 추가하는 코드를 테스트한다고 하면, 가짜 데이터베이스(Mock Database)를 주입하여 insert 처리 시에 반드시에 1을 반환하도록 해주는 것이 stub이다.
좋은 단위 테스트의 특징
일반적으로 요구 사항은 계속해서 변하고, 그에 맞춰 우리의 코드 역시 변경되어야 한다. 하지만 실제 코드를 변경한다는 것은 잠재적인 버그가 발생할 수 있음을 내포하는데, 좋은 테스트 코드가 있다면 변경된 코드를 검증함으로써 이를 해결할 수 있다. 또한 실제 코드가 변경되면 테스트 코드 역시 변경이 필요할 수 있는데, 이러한 이유로 우리는 테스트 코드 역시 가독성있게 작성할 필요가 있다.
그렇기에 테스트를 작성하는 경우에는 다음을 준수하는 것이 좋다.
- 1개의 테스트 함수에 대해 assert를 최소화하라
- 1개의 테스트 함수는 1가지 개념 만을 테스트하라
또한 좋고 깨끗한 테스트 코드는 FIRST라는 5가지 규칙을 따라야 한다.
- Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야 한다.
- Independent: 각각의 테스트는 독립적이며 서로 의존해서는 안된다.
- Repeatable: 어느 환경에서도 반복 가능해야 한다.
- Self-Validating: 테스트는 성공 또는 실패로 bool 값으로 결과를 내어 자체적으로 검증되어야 한다.
- Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
통합 테스트
런던파의 통합 테스트 : 실제 협력자 객체를 사용하는 모든 테스트를 통합 테스트이다. 고전파는 런던파 입장에서 모두 통합 테스트이다.
고전파의 통합 테스트 : 공유 의존성 접근하는 테스트 또는 둘 이상의 동작 단위 검증할 때 통합 테스트이다.
엔드 투 엔드 테스트(end-to-end test)
엔드 투 엔드 테스트는 공유 의존성뿐만 아니라 조직 내 다른 팀이 개발한 코드 등과 통합해 작동하는 테스트다. 일반 통합 테스트보다 더 많은 의존성이 있다. UI(User Interface), GUI(Graphic User Interface) 테스트라고도 할 수 있다.
엔드 투 엔드 테스트 작성에 가장 많은 비용이 발생하기 때문에 모든 단위 테스트, 통합 테스트 통과한 후 후반에 작성, 실행하는 것이 좋다. 하지만 엔드 투 엔드 테스트의 의존성을 원하는 상태를 만들 수 없을 수 있기 때문에 테스트 대역을 쓸 때도 있다. 통합 테스트와 엔드 투 엔드 테스트를 정확히 구분하기 어려울 수 있다.
'좋은 개발자가 되기' 카테고리의 다른 글
객체지향의 사실과 오해(7장) (0) | 2023.05.16 |
---|---|
객체지향의 사실과 오해(6장) (0) | 2023.05.09 |
객체지향의 사실과 오해(5장) (0) | 2023.05.01 |
객체지향의 사실과 오해(4장) (0) | 2023.04.25 |
객체지향의 사실과 오해(3장) (2) | 2023.04.17 |