《编写可读代码的艺术》第14章 测试与可读性

1. 使测试易于阅读和维护

    测试代码的可读性和被测试代码同样重要。很多程序员会把测试代码看做非正式文档,它记录了代码如何工作及如何使用。

    当测试代码多得让人望而却步,程序员不敢修改真实代码,不会再增加新的测试。从而对测试代码丧失信心。

2. 有问题的测试代码示例,后面要一一对它们进行修改。

 1 void Test1() {
 2     vector<ScoredDocument> docs;
 3     docs.resize(5);
 4     docs[0].url = "http://example.com";
 5     docs[0].score = -5.0;
 6     docs[1].url = "http://example.com";
 7     docs[1].score = 1;
 8     docs[2].url = "http://example.com";
 9     docs[2].score = 4;
10     docs[3].url = "http://example.com";
11     docs[3].score = -99998.7;
12     docs[4].url = "http://example.com";
13     docs[4].score = 3.0;
14 
15     SortAndFilterDocs(&docs);
16     
17     assert(docs.size() == 3);
18     assert(docs[0].score == 4);
19     assert(docs[1].score == 3.0);
20     assert(docs[2].score == 1);
21 }

  3. 使这个测试更可读

    对使用者隐去不重要的细节,以便更重要的细节能突出。这样测试代码变得紧凑一点了。

 1 void MakeScoredDoc(ScoredDocument* sd, double score, string url) {
 2     sd->score = score;
 3     sd->url = url;
 4 }
 5 
 6 void Test1() {
 7     vector<ScoredDocument> docs;
 8     docs.resize(5);
 9     MakeScoredDoc(&docs[0], -5.0, "http://example.com");
10     MakeScoredDoc(&docs[1], 1, "http://example.com");
11     MakeScoredDoc(&docs[2], 4, "http://example.com");
12     MakeScoredDoc(&docs[3], -99998.7, "http://example.com");
13     ...
14 }

    进一步隐藏细节

 1 void AddScoredDoc(vector<ScoredDocument>& docs, double score) {
 2     ScoredDocument sd;
 3     sd.score = score;
 4     sd.url = "http://example.com";
 5     docs.push_back(sd);
 6 }
 7 
 8 void Test1() {
 9     vector<ScoredDocument> docs;
10     AddScoredDoc(docs, -5.0);
11     AddScoredDoc(docs, 1);
12     AddScoredDoc(docs, 4);
13     AddScoredDoc(docs, -99998.7);
14     ...
15 }

4. 创建最小的测试声明

    CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");

    我们可以把测试的基本内容精简为一行代码来描述,即对于给定的输入,期望的输出是什么。

5. 实现定制的“微语言”

    在较新版本的C++中,可以这样传入数组:

    CheckScoresBeforeAfter({-5, 1, 4, -99998.7, 3}, {4, 3, 1});

    然而在之前,需要对字符串进行解析:

 1 vector<ScoredDocument> ScoredDocsFromString(string scores) {
 2     vector<ScoredDocument> docs;
 3     replace(scores.begin(), scores.end(), ',', ' ');
 4     // Populate 'docs' from a string of space-separated scores.
 5     istringstream stream(scores);
 6     double score;
 7     while (stream >> score) {
 8         AddScoredDoc(docs, score);
 9     }
10     return docs;
11 }
12 
13 string ScoredDocsToString(vector<ScoredDocument> docs) {
14     ostringstream stream;
15     for (int i = 0; i < docs.size(); i++) {
16         if (i > 0) {
17             stream << ", ";
18             stream << docs[i].score;
19         }
20     }
21     return stream.str();
22 }
23 
24 void CheckScoresBeforeAfter(string input, string expected_output) {
25     vector<ScoredDocument> docs = ScoredDocsFromString(input);
26     SortAndFilterDocs(&docs);
27     string output = ScoredDocsToString(docs);
28     assert(output == expected_output);
29 }

    乍一看有很多的代码,但是实际代码的能力更强了,你可以只调用。

    CheckScoresBeforeAfter一次就写出了整个测试,你将倾向于增加更多的测试代码。

6. 让错误消息具有可读性

  assert(output == expected_output);

    Assertion failed: (output == expected_output),

   function CheckScoresBeforeAfter, file test.cc, line 37.

    大部分语言都有高级版本的assert()可用。

    BOOST_REQUIRE_EQUAL(output, expected_output)

  test.cc(37): fatal error in "CheckScoresBeforeAfter": critical check
    output == expected_output failed ["1, 3, 4" != "4, 3, 1"]

   手工打造错误消息。理想的错误消息可以像这样:

  CheckScoresBeforeAfter() failed,
    Input:
    "-5, 1, 4, -99998.7, 3"
    Expected Output: "4, 3, 1"
    Actual Output: "1, 3, 4"

 1 void CheckScoresBeforeAfter(...) {
 2     //...
 3     if (output != expected_output) {
 4         cerr << "CheckScoresBeforeAfter() failed," << endl;
 5         cerr << "Input:
 6         "" << input << """ << endl;
 7         cerr << "Expected Output: "" << expected_output << """ << endl;
 8         cerr << "Actual Output: "" << output << """ << endl;
 9         abort();
10     }
11 }

7. 选择好的测试输入

    应当选择一组最简单的输入,它能完整地使用被测代码。

1 CheckScoresBeforeAfter("1, 2, 3", "3, 2, 1"); //未测试负分的情况
2 CheckScoresBeforeAfter("123014, -1082342, 823423, 234205, -235235",
3     "823423, 234205, 123014"); // 无必要的复杂

     简化输入值

CheckScoresBeforeAfter("-5, 1, 4, -99998.7, 3", "4, 3, 1");

// -99998.7, 3只是想表示很大的负数,用-1e100这样的值更好
// 实际上只需要一个负数来测试负数会被移除就可以了:
CheckScoresBeforeAfter("1, 2, -1, 3", "3, 2, 1");

//你可能会想包含这样一个测试,
//大型输入在发现bug方面很有用,但是这样的代码大多看上去很吓人,效果却不好
CheckScoresBeforeAfter("100, 38, 19, -25, 4, 84, [lots of values] ...",
    "100, 99, 98, 97, 96, 95, 94, 93, ...");
// 用编程的方法来生成大型输入(如100000个值)会有更好的效果。

    一个功能的多个小测试,更容易、更高效且更有可读性

    SortAndFilterDocs()的四个测试: 

1 CheckScoresBeforeAfter("2, 1, 3", "3, 2, 1");   //Basic sorting
2 CheckScoresBeforeAfter("0, -0.1, -10", "0");    // All values < 0 removed
3 CheckScoresBeforeAfter("1, -2, 1, -2", "1, 1"); //Duplicates not a problem
4 CheckScoresBeforeAfter("","");                  //Empty input O

 8. 为测试函数命名

    不要用Test1()这样的名字,可以用Test_<FunctionName>()这样的格式:

    void Test_SortAndFilterDocs()

    或者Test_<FunctionName>_<Situation>()这样的格式:

    void Test_SortAndFilterDocs_BasicSorting()
    void Test_SortAndFilterDocs_NegativeValues()

    在你的整个代码库中不会调用这些函数,因此避免使用长函数名的情形在这里不适用

9. 对测试较好的开发方式

    对测试友好的东西往往自然地产生有良好组织的代码。

    尽量避免:1. 使用全局变量;2. 对外部组件有大量依赖的代码;3. 代码有不确定的行为。

    应当做到:1. 类中只有很少或者没有内部状态;2. 一个类/函数只做一件事;3. 每个类对别的类的依赖很少(低耦合);4. 函数的接口简单,定义明确。

    不要走得太远:1. 为了测试,牺牲代码可读性;2. 着迷于100%的测试覆盖率;3. 让测试成为产品开发的阻碍

 
原文地址:https://www.cnblogs.com/yyqng/p/14286834.html