本周技巧 #122:测试夹具、清晰度和数据流

最初以 totw/122 发布于 2016-08-30

作者:Titus Winters (titus@google.com)

更新日期:2017-10-20

快速链接:abseil.io/tips/122

清晰晦涩。- E.B. 怀特

测试代码与生产代码有何不同?首先,测试是未经测试的:当你编写的乱七八糟的意大利面条代码分布在多个文件中,并且有数百行的 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_ 进行了比较......我们又在那里存储了什么?

与以更简单的方式编写的等效代码进行比较:

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


有了这种风格,即使在有数百个测试的世界里,我们也能仅通过局部信息就准确地知道发生了什么。

首选自由函数


在前面的示例中,所有变量的初始化都是简洁明了的。在实际测试中,情况并非总是如此。不过,关于数据流和避免固定装置的想法可能同样适用。请看下面这个 protobuf 例子:

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(),而且我们使用了一个固定装置和固定装置成员变量,如果不检查 SomeCall() 的细节,就无法确定 bobber1_ 和 bobber2_ 在初始化和 EXPECT_THAT 验证之间是否被修改过。这可能需要更多的滚动操作。

请考虑

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 通常会更好,你的测试几乎总是更清晰。
  • 20
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值