C++ 测试驱动开发 TDD(一)

文章目录

        • TDD 介绍
        • Soundex 算法示例介绍
        • 增加Soundex 算法测例1
        • 增加Soundex 算法测例2
        • Soundex 算法测例1 、2重构
        • 后记

最近阅读了《C++程序设计实践与技巧:测试驱动开发》,也算是第一次系统的了解到 TDD 的概念吧,因为整个调试的过程太细了,想要一次性说明白着实需要一点功力,这里权当一些读后感分享,有兴趣的读者可以自己找到这本书来读读看。 虽然不能说可以立马有所了解,或者就可以在实际业务代码中就用上了,但是总得先有这么一个了解过程嘛。

TDD 介绍

感觉 TDD 更多的是从写测例的角度出去去思考自己要做的事情,需要如何实现更完备的开发代码。包括:

  1. 设计合理的测例
    根据当前已经实现的功能,及接下来需要实现的规则,增量地增加当前代码不过通过的测例,然后修改开发代码来使新增的测例通过,通过设计逐渐完善的测例来引导开发功能的完善。
  2. 让测例通过
    这里面有一系列的编程方法在里面,如何拓展开发代码使当前测例通过,通过以后目前的代码是否可以重构的更好,保持代码的单一责任原则。
  3. 重构当前代码
    代码单一责任原则,修改合适的变量及函数,甚至类的接口权限与拓展,去除代码里面的“坏味道”,修改以后确保当前测例还是都可以通过的。

然后继续设计新的测例,重复上述 3 个步骤。

书中说到,在 TDD 上述步骤的每一小步,必须可以回答一下的问题:

  • 写一个小的测试
    怎么样才算小的增量行为?系统中已经存在这样的行为了吗?怎么让测试名称准确表达行为?测试中使用的接口是客户端代码使用这一行为的最好方式吗?
  • 确保新的测试是失败的
    如果没有失败,为什么?这个行为系统中已经存在了?忘记编译了?是不是在上个测试步子迈大了?断言是否有效?
  • 写出你认为可以让测试通过的代码
    你写的代码是不是刚好满足测试说明的行为要求?你清楚刚才写的代码哪里需要整理吗?你遵循团队的标准了吗?
  • 确保所有测试都通过
    如果没有,你的编码正确吗?或者你的规范正确吗?
  • 整理刚才的代码改动
    怎样做才能让你的代码符合团队标准?新的代码和系统中其他要清楚的代码有重复吗?代码有没有坏味道?遵循好的设计原则了吗?除了当下要做的设计和代码整理工作,你还知道其他什么?设计是朝好的方向发展的吗?你的代码改动会导致需要修改其他地方的代码吗?
  • 确保所有测试再次通过
    确信你的单元测试覆盖率够高吗?你是不是应该运行一些速度较慢的测试集合?下一步测试是什么?

Soundex 算法示例介绍

soundex 算法是一种语音算法,这个算法遵循以下 4 种规则:

  • 保留第一个字母,丢掉所有出现的a、e、i、o、u、y、h、w
  • 以数字来代替辅音(第一个字母除外):
    b f p v -> 1
    c g j k q s x z -> 2
    d t -> 3
    l -> 4
    m n -> 5
    r -> 6
  • 对于相邻的重复的数字只保留一个,即相邻的两个被替换为同一个数字的字母只保留一个; 如果出现两个编码相同的字母,且它们被 h 和 w 隔开,也这样处理;但如果被元音隔开,就要编码两次。这条规则同样适用于第一个字母。
  • 当得到一个字母和单个数字时,停止处理。如果需要,补零以对齐。

增加Soundex 算法测例1

想要实现上述功能,如果是你,你将如何设计第一个测例呢?
下面是作者给的步骤:
当前无任何开发代码,那么我需要一个Soundex类来实现需要的 Soundex 算法,直接用 Soundex 初始化一个对象,这个测例必然是会挂的(步骤1);为了使这个测试快速通过,我肯定是需要创建一个 Soundex 类(步骤2)

  1. 构建第一个测例
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
}
  1. 写出对应让测例通过开发代码
    这个代码可以暂时先让测例通过,不用一步跨很大,想着一次就把类的成员变量和成员函数设计好,这个后面设计新的测例会驱动你逐渐完善的,这里为了方便调试,暂时把类定义与测例放在一个文件中。
class Soundex {};

TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
}

ok,完成上述代码测例就可以通过了。然后我们就可以编译运行 gtest,确认测例通过了。我们已经迈出了 TDD 的伟大第一步了!
3. 设计下一个测例
有了上面的结构,可以开始思考对这个类的行为该设计怎样的接口了,类的核心就是要用来编码我们的输入,所以要先测试一个 encode 接口,首先编码一个字母。

TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
  auto encoded = soundex.encode("A");  // 新增代码
}

这时类还没有声明和实现 encode 接口,所以编译测例就会挂掉(步骤1),这时我们根据规则, 来先实现 encode 接口,使编译先通过:

class  Soundex {
 public:  // 新增代码
   std::string encode(const std::string& word) const {. // 新增代码
     return "";  // 新增代码
   } // 新增代码
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
  auto encoded = soundex.encode("A");  // 新增代码
}

这样之后我们可以在此编译,这次测例可以顺利编译了,是时候验证一些有用的东西了。

class  Soundex {
 public:
   std::string encode(const std::string& word) const { 
     return ""; 
   }
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
  auto encoded = soundex.encode("A"); 
  ASSERT_THAT(encoded, testing::Eq("A"));  // 新增代码
}

当前的逻辑当然不满足这个条件,运行直接报错,提示 “” 不等于 “A”,为了让测例通过我们就要修改 encode 接口的代码,可以直接把 return "";修改为 return "A"; ,但是这仅仅只支持一个特定的单字母, return word; 岂不是更好?

class  Soundex {
 public:
   std::string encode(const std::string& word) const { 
     return word;  // 新增代码
   }
};
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
  auto encoded = soundex.encode("A"); 
  ASSERT_THAT(encoded, testing::Eq("A")); 
}

这样测例又可以通过了。经过上面那么多看似没必要的描述过程,正是 TDD 的核心所在 —— 增量性。 第一次接触会觉着这样的方式有点不自然而且速度很慢,但是随着试驾你的推移,小步的增量开发反而能够加快你的速度! 开发代码到这里完全还没有 Soundex 算法 4 个规则的影子。

增加Soundex 算法测例2

增量的开发思路可以使我们以任何顺序一点点地开发系统,并持续地验证,向前推进。所以这时我们可以开始为新的行为写一个新的测试,并修改已有的测例来满足规范了。根据规则 4,新的测例如下:

TEST(SoundexEncoding, PadWithZerosToEnsureThreeDigits) { // 新增测例
  Soundex soundex;
  auto encoded = soundex.encode("I");
  ASSERT_THAT(encoded, testing::Eq("I000")); 
}

直接运行新增测例会挂掉,会提示 encode 返回的是 “I” 而不是 “I000”,修改当前代码很简单,先采用硬编码:

std::string encode(const std::string& word) const { 
     return word +000;  // 新增代码
   }

新增示例可以通过,但之前的示例 1 就会挂掉,而让示例 1 通过的方式也很简单,把ASSERT_THAT(encoded, testing::Eq("A")); 变成 ASSERT_THAT(encoded, testing::Eq("A000")); 就可以了。

这时示例1 和示例 2 大体相同,只是数据不同,但是没关系,因为每个测试分别描述了一种行为(这就是所谓的测试代码成为文档),保留下来不仅确保系统按预期工作,还可以让每个人知道所有既定的系统行为

是时候重构了。

Soundex 算法测例1 、2重构

主要重构的点有以下几点:

  • 可以使用 using 来减少一些 namespace 的拼写
  • 每一个测例中都会重新创建一个 Soundex 类的对象,完全可以通过测试框架的语法来避免这一重复创建的过程
  • 提炼更单一责任的单函数出来

重构前:

class  Soundex {
 public:
   std::string encode(const std::string& word) const { 
     return word +000;
   }
};

TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  Soundex soundex;
  auto encoded = soundex.encode("A"); 
  ASSERT_THAT(encoded, testing::Eq("A000")); 
}

TEST(SoundexEncoding, PadWithZerosToEnsureThreeDigits) {
  Soundex soundex;
  auto encoded = soundex.encode("I");
  ASSERT_THAT(encoded, testing::Eq("I000")); 
}

重构后,为了代码干净,可以把开发代码,也就是 Soundex 类的定义与测例文件分开成两个文件了:

#include "Soundex.h"

using namespace testing;

class SoundexEncoding : public Test {
 public:
   Soundex soundex;
}

TEST_F(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
  ASSERT_THAT(soundex.encode("A"), Eq("A000")); 
}

TEST_F(SoundexEncoding, PadWithZerosToEnsureThreeDigits) {
  ASSERT_THAT(soundex.encode("A"), Eq("I000"));  
}

Soundex.h

#include 

class  Soundex {
 public:
   std::string encode(const std::string& word) const { 
     return zeroPad(word);
   }
   
 private:
   std::string zeroPad(const std::string& word) const {
     return word +000;
   }
};

后记

基本上上述的内容是 TDD 流程的一个缩影,包含了 TDD 的核心三步,可以让读者有了一个大概的了解,当然我们目前只是处理了规则 4 的一部分,还有后续的规则需要处理,文章太长会影响阅读体验,所以如果仅仅是想要对 TDD 有一个大概了解,读到这里就足够了,下面的文章会继续我们的 Soundex 类的开发之旅。

C++ 测试驱动开发 TDD(二)

原书中还有关于如何编写高质量测例的内容,以及如何处理对别人遗留代码进行测试,重构的相关内容,感兴趣的读者可以阅读原文或者留言需要博客概括。水平有限,欢迎留言交流学习。

你可能感兴趣的:(C++,读书笔记)