[단위 테스트] 단위 테스트 정의와 활용
'단위 테스트' 정의
-> 작은 코드 조각을 검증하고
-> 빠르게 수행하고
-> 격리된 방식을 처리하는 자동화된 테스트
고전파 vs 런던파
런던파에서는 테스트 대상 시스템(SUT)를 협력자에게서 격리하는 것을 의미. 모든 협력자는 테스트 대역으로 대체.
쉽게 말해서 테스트 대상 객체가 아닌 다른 객체에 의해 테스트가 실패할 요인을 없애는 것.
ex) 타이어(Tire) 객체에 의존하는 자동차(Car) 객체
class Tire {
public:
virtual void run() {
}
};
class Car {
private:
Tire tire;
public:
Car(Tire _tire) : tire(_tire) {
}
void run() {
tire.run();
}
};
런던파에서는 Tire를 테스트 대역(Test Double)으로 대체하여 테스트 해야한다고 생각한다.
class FakeTire : public Tire {
public:
virtual void run() override {
}
};
int main() {
FakeTire t;
Car car(t);
car.run();
return 0;
}
Google Test를 이용한 유닛 테스트 작성하기
1. 기본 테스트 대상이 되는 프로젝트를 준비한다.
2. Google Test 테스트 프로젝트를 추가한다.
3. GMock 라이브러리 추가
VisualStudio에서 Nuget 패키지 관리자를 통해 GMock 라이브러리를 추가한다.
참조, 포함 헤더, 목적 파일 참조 추가
4. 테스트 코드 준비
다음 헤더를 포함하여 준비한다.
#pragma once
#include "gtest/gtest.h"
#include "gmock/gmock.h"
상점 테스트 시나리오
- 손님이 상점에서 물건을 사는 시나리오를 대상으로 유닛 테스트 코드를 작성해본다.
- 테스트 코드의 패턴은 AAA (Aggregate, Act, Assert) 패턴으로 되어 있다.
- 혹은 Given/When/Then으로 명명하기도 한다.
고전적 스타일의 단위 테스트
TEST(상점_시나리오_고전파, 여유있는_인벤_구매_성공) {
// given
Store store = {};
store.AddInventory(Product::Shampoo, 10);
Customer customer = {};
// when
bool success = customer.Purchase(store, Product::Shampoo, 5);
// then
ASSERT_TRUE(success);
ASSERT_EQ(5, store.GetInventory(Product::Shampoo));
}
TEST(상점_시나리오_고전파, 빈약한_인벤_구매_실패) {
// given
Store store = {};
store.AddInventory(Product::Shampoo, 10);
Customer customer = {};
// when
bool success = customer.Purchase(store, Product::Shampoo, 15);
// then
ASSERT_FALSE(success);
ASSERT_EQ(10, store.GetInventory(Product::Shampoo));
}
고전적 스타일의 유닛 테스트에서 SUT 대상 Customer가 Purchase라는 함수가 제대로 동작하는 지 테스트 하게 된다.
성공과 실패 두 시나리오는 Store 협력 객체가 오류가 없다면 모두 성공하는 결과를 볼 수 있다.
하지만, SUT가 의존하는 Store 객체가 내부에 오류가 있다면 Customer의 정상적인 동작과 상관없이 단위 테스트가 실패하게 될 것이다.
이를 방지하기 위해 런던파의 스타일에서는 두 의존성을 제거하기 위해 실제 Store 객체 대신 Mock이라는 가짜 객체로 대체한다.
런던 스타일의 단위 테스트
TEST(상점_시나리오_런던파, 여유있는_인벤_구매_성공) {
// given
MockStore store;
ON_CALL(store, HasEnoughInventory(Product::Shampoo, 5))
.WillByDefault(Return(true));
Customer customer = {};
EXPECT_CALL(store, RemoveInventory(Product::Shampoo, 5))
.Times(1).WillOnce(Return());
// when
bool success = customer.Purchase(store, Product::Shampoo, 5);
// then
ASSERT_TRUE(success);
}
TEST(상점_시나리오_런던파, 빈약한_인벤_구매_실패) {
// given
MockStore store = {};
ON_CALL(store, HasEnoughInventory(Product::Shampoo, 15))
.WillByDefault(Return(false));
Customer customer = {};
EXPECT_CALL(store, RemoveInventory(Product::Shampoo, 15))
.Times(0);
// when
bool success = customer.Purchase(store, Product::Shampoo, 15);
// then
ASSERT_FALSE(success);
}
Store 객체는 MockStore 객체로 대체되었다. Mock 객체는 테스트 전에 ON_CALL과 같은 방법으로 미리 행동 양식을 정의한다. 실제 내용은 아무것도 없고 테스트를 위한 특정 인수를 받고 그 결과가 어떤 것이라는 함수 입력-결과 쌍의 명세만 주어진다. (HasEnoughInventory(~,~)를 호출했을 때 false를 반환할 것이다.)
고전파에서는 그 결과를 상점의 상태 (GetInventory)를 확인하여 그 결과를 하였다면, 런던파에서는 Purchase를 호출했을 때, Store 객체와 Customer의 상호작용에 중점을 두고 Customer가 결국 Store의 RemoveInvetory를 호출했는 지를 검증하게 된다.
Customer 객체가 Store 객체에 의존하는 것을 공유 의존성이 이라고한다. 런던파에서는 이러한 공유 의존성을 제거하고 오로지 테스트 대상 단위 SUT에 초점을 맞추어야한다고 본다.
TDD (Test Driven Development)
TDD에서 개발 과정은 보통 다음과 같다.
1. 테스트 명세 작성
2. 테스트를 통과하는 실제 로직 작성
3. 통과하는 테스트 케이스를 바탕으로 로직 코드를 리팩토링한다.
보통 고전파에서는 먼저 로직을 작성하고 테스트 명세를 작성하는 순서로 상향식으로 이루어지고 런던파에서는 테스트 명세부터 작성하여 하향식으로 이루어진다 한다고 한다.
고전파와 런던파 비교
- 고전파
-> 테스트 시나리오를 직관적으로 작성할 수 있고 그 결과를 통해 둘 이상의 클래스에 대해 실제 역할을 하는 객체로 테스트하기 때문에 효과적으로 검증 가능하다.
-> 격리되지 않은 둘 이상의 객체 중 하나의 객체의 오류로 인해 버그를 추적하기 쉽지 않을 수 있다.
-> 실제 로직을 테스트하는 코드 단위의 테스트이다.
- 런던파
-> 테스트가 하나의 클래스에 집중되기 때문에 세밀한(fine-grained) 테스트가 가능하다. 버그를 추적하기 쉽다.
-> 코드 단위의 테스트가 아니라 동작 행위를 검증하게 된다. 주로 테스트 대상 단위가 의존하는 협력자의 동작을 정의해야하기 때문에 테스트 코드에서 숨겨진 협력자의 호출 코드가 나타난다(결합도가 증가한다).
참고
Unit Testing 단위 테스트, 블라디미르 코리코프 지음
https://github.com/jinsnowy/UnitTest_Study