Abseil Tip 122 테스트 픽스처, 명확성, 그리고 데이터 흐름

한글 번역


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()));
}

초기화를 자유 함수로 옮기면 숨겨진 데이터 흐름이 없다는 점이 명확해집니다. 도우미 함수의 적절한 이름은 테스트를 검토할 때 함수의 세부 사항을 확인하지 않아도 되게 해줍니다.

다섯 가지 간단한 단계

테스트의 명확성을 개선하는 방법은 다음과 같습니다:

  1. 가능하다면 픽스처를 피하세요. 항상 가능한 것은 아닙니다.
  2. 픽스처를 사용할 경우, 픽스처 멤버 변수를 피하세요. 이는 전역 변수처럼 작동하여 데이터 흐름을 추적하기 어렵게 만듭니다.
  3. 복잡한 초기화가 필요한 변수가 있다면, 픽스처가 아닌 도우미 함수를 사용하여 해당 객체를 직접 반환하도록 하세요.
  4. 멤버 변수를 포함한 픽스처가 필요한 경우, 멤버 변수에 직접 접근하는 메서드는 피하고, 가능한 한 매개변수로 전달하세요.
  5. 헤더 파일을 작성하기 전에 테스트를 작성하세요. 테스트가 쉽게 작성된다면, API는 더 나아지고 테스트는 항상 더 명확해집니다.

reference

https://raw.githubusercontent.com/abseil/abseil.github.io/refs/heads/master/_posts/2017-10-20-totw-122.md