title: “이번 주의 팁 #122: 테스트 픽스처, 명확성, 그리고 데이터 흐름” layout: tips sidenav: side-nav-tips.html published: true permalink: tips/122 type: markdown order: “122” —
원래 totw/122로 2016-08-30에 게시됨
작성자: Titus Winters (titus@google.com)
2017-10-20 업데이트됨
빠른 링크: abseil.io/tips/122
“명확하게 모호하라.” — E.B. White
테스트 코드와 프로덕션 코드의 차이점은 무엇일까요?
가장 큰 차이 중 하나는 테스트는 테스트되지 않는다는 점입니다. 여러 파일에 걸쳐 퍼져 있는 지저분한 스파게티 코드를 작성하고, 수백 줄의 SetUp
코드가 있을 때, 이 테스트가 정말로 필요한 것을 테스트하고 있는지 누가 확신할 수 있을까요? 종종 코드 리뷰어들은 세팅이 타당하다고 가정하고, 개별 테스트 케이스의 논리를 대충 확인할 수밖에 없습니다. 이런 경우, 무언가 변경되면 테스트는 실패할 가능성이 높지만, 그 변경 사항이 올바른 것인지 명확하지 않은 경우가 많습니다.
반면에 각 테스트를 단순하고 가능한 한 직관적으로 유지하면, 테스트가 올바른지 눈으로 확인하기 쉽고, 논리를 이해하기 쉽습니다. 또한 더 나은 품질의 테스트 논리를 검토할 수 있습니다. 이를 달성할 수 있는 몇 가지 간단한 방법을 살펴보겠습니다.
픽스처에서의 데이터 흐름
다음 예제를 보겠습니다:
class FrobberTest : public ::testing::Test {
protected:
void ConfigureExampleA() {
example_ = "Example A";
frobber_.Init(example_);
expected_ = "Result A";
}
void ConfigureExampleB() {
example_ = "Example B";
frobber_.Init(example_);
expected_ = "Result B";
}
Frobber frobber_;
string example_;
string expected_;
};
TEST_F(FrobberTest, CalculatesA) {
ConfigureExampleA();
string result = frobber_.Calculate();
EXPECT_EQ(result, expected_);
}
TEST_F(FrobberTest, CalculatesB) {
ConfigureExampleB();
string result = frobber_.Calculate();
EXPECT_EQ(result, expected_);
}
이 간단한 예제에서 우리의 테스트는 30줄에 걸쳐 있습니다. 10배 더 복잡한 예제를 쉽게 상상할 수 있고, 이는 단일 화면에 다 담기 어렵습니다. 이 코드의 정확성을 검증하려는 독자나 코드 리뷰어는 다음과 같은 절차를 따라야 합니다:
- “이것은 FrobberTest인데, 어디에 정의되어 있지…? 아, 이 파일이군.”
- “
ConfigureExampleA
… FrobberTest 메서드네. 멤버 변수에 접근하고 있군. 그 변수들의 타입은 무엇일까? 어떻게 초기화되지? 오, Frobber와 두 개의 문자열이군.SetUp
이 있는지 확인… 기본 생성이네.” - “다시 테스트로 돌아가서… 결과를 계산하고
expected_
와 비교하네…expected_
에 뭐가 저장되어 있더라?”
비교를 위해 더 단순한 스타일로 작성된 코드를 보겠습니다:
TEST(FrobberTest, CalculatesA) {
Frobber frobber;
frobber.Init("Example A");
EXPECT_EQ(frobber.Calculate(), "Result A");
}
TEST(FrobberTest, CalculatesB) {
Frobber frobber;
frobber.Init("Example B");
EXPECT_EQ(frobber.Calculate(), "Result B");
}
이 스타일에서는 수백 개의 테스트가 있더라도, 각 테스트가 무엇을 하는지 로컬 정보만으로 쉽게 알 수 있습니다.
자유 함수 선호
이전 예제에서 모든 변수 초기화는 간결하게 되어 있었습니다. 하지만 실제 테스트에서는 항상 그렇지 않습니다. 그러나 데이터 흐름과 픽스처 사용을 피하는 동일한 아이디어를 적용할 수 있습니다. 다음 프로토콜 버퍼 예제를 보겠습니다:
class BobberTest : public ::testing::Test {
protected:
void SetUp() override {
bobber1_ = PARSE_TEXT_PROTO(R"(
id: 17
artist: "Beyonce"
when: "2012-10-10 12:39:54 -04:00"
price_usd: 200)");
bobber2_ = PARSE_TEXT_PROTO(R"(
id: 21
artist: "The Shouting Matches"
when: "2016-08-24 20:30:21 -04:00"
price_usd: 60)");
}
BobberProto bobber1_;
BobberProto bobber2_;
};
TEST_F(BobberTest, UsesProtos) {
Bobber bobber({bobber1_, bobber2_});
SomeCall();
EXPECT_THAT(bobber.MostRecent(), EqualsProto(bobber2_));
}
중앙 집중화된 리팩토링은 많은 간접성을 유발합니다. 선언과 초기화가 분리되어 있고 실제 사용과 멀리 떨어져 있을 수 있습니다. 또한 SomeCall()
때문에 bobber1_
과 bobber2_
가 초기화 후 변경되지 않았는지 확인하기 위해 더 많은 확인이 필요합니다.
다음과 같이 변경해 보세요:
BobberProto RecentCheapConcert() {
return PARSE_TEXT_PROTO(R"(
id: 21
artist: "The Shouting Matches"
when: "2016-08-24 20:30:21 -04:00"
price_usd: 60)");
}
BobberProto PastExpensiveConcert() {
return PARSE_TEXT_PROTO(R"(
id: 17
artist: "Beyonce"
when: "2012-10-10 12:39:54 -04:00"
price_usd: 200)");
}
TEST(BobberTest, UsesProtos) {
Bobber bobber({PastExpensiveConcert(), RecentCheapConcert()});
SomeCall();
EXPECT_THAT(bobber.MostRecent(), EqualsProto(RecentCheapConcert()));
}
초기화를 자유 함수로 옮기면 숨겨진 데이터 흐름이 없다는 점이 명확해집니다. 도우미 함수의 적절한 이름은 테스트를 검토할 때 함수의 세부 사항을 확인하지 않아도 되게 해줍니다.
다섯 가지 간단한 단계
테스트의 명확성을 개선하는 방법은 다음과 같습니다:
- 가능하다면 픽스처를 피하세요. 항상 가능한 것은 아닙니다.
- 픽스처를 사용할 경우, 픽스처 멤버 변수를 피하세요. 이는 전역 변수처럼 작동하여 데이터 흐름을 추적하기 어렵게 만듭니다.
- 복잡한 초기화가 필요한 변수가 있다면, 픽스처가 아닌 도우미 함수를 사용하여 해당 객체를 직접 반환하도록 하세요.
- 멤버 변수를 포함한 픽스처가 필요한 경우, 멤버 변수에 직접 접근하는 메서드는 피하고, 가능한 한 매개변수로 전달하세요.
- 헤더 파일을 작성하기 전에 테스트를 작성하세요. 테스트가 쉽게 작성된다면, API는 더 나아지고 테스트는 항상 더 명확해집니다.
reference
https://raw.githubusercontent.com/abseil/abseil.github.io/refs/heads/master/_posts/2017-10-20-totw-122.md