Skip to content

Latest commit

 

History

History
577 lines (427 loc) · 28.9 KB

File metadata and controls

577 lines (427 loc) · 28.9 KB

十一、基于属性的测试

我们已经看到,纯函数有一个重要的特性——对于相同的输入,它们返回相同的输出。我们还看到,这个属性允许我们轻松地为纯函数编写基于示例的单元测试。此外,我们可以编写数据驱动的测试,允许一个测试函数被多个输入和输出重用。

事实证明,我们可以做得更好。我们可以利用纯函数的数学特性,而不是或者除此之外,编写许多行数据驱动的测试。这种技术是可能的,因为数据发生器是由函数编程实现的。这些测试被混乱地命名为基于属性的测试;你必须记住,这个名字来自纯函数的数学属性,而不是在类或对象中实现的属性。

本章将涵盖以下主题:

  • 理解基于属性的测试的思想
  • 如何编写生成器并加以利用
  • 如何从基于示例的测试获得基于属性的测试
  • 如何写出好的属性

技术要求

您将需要一个支持 C++ 17 的编译器。我用的是 GCC 7.4.0。

代码可以在的 GitHub 上找到。com/ PacktPublishing/动手-函数-用- Cpp 编程Chapter11文件夹中的。它包括并使用doctest,这是一个单头开源单元测试库。你可以在它的 GitHub 资源库上找到它。

基于属性的测试

单元测试是一种非常有用的软件开发技术。一套好的单元测试可以做到以下几点:

  • 通过自动化回归测试的枯燥部分来加快部署。
  • 使专业测试人员能够发现隐藏的问题,而不是一次又一次地运行相同的测试计划。
  • 在开发过程中尽早移除 bug,从而降低发现和修复 bug 的成本。
  • 通过作为代码结构的第一个客户提供反馈来改进软件设计(如果测试很复杂,很可能您的设计也很复杂),只要开发人员知道如何查看和解释反馈。
  • 增加对代码的信任,从而允许更多的更改,从而促进重构,加快开发速度或消除代码中的风险。

我喜欢写单元测试。我喜欢弄清楚有趣的测试用例,我喜欢用测试来驱动我的代码——正如你在第 9 章功能编程的测试驱动开发中看到的那样。与此同时,我一直在寻找更好的方法来编写测试,因为如果我们能加快这个过程就太好了。

我们已经在第 9 章功能编程的测试驱动开发中看到,纯函数允许我们更容易地识别测试用例,因为根据定义,它们的输出是受约束的。事实证明,如果我们冒险进入与这些纯函数相关的数学性质领域,我们可以走得更远。

如果你已经写单元测试有一段时间了,你可能会觉得有些测试有点多余。如果我们能编写这样的测试就好了——对于某个值区间内的输入,期望的输出必须具有某个属性。事实证明,在数据生成器和一些抽象思维的帮助下,我们可以做到这一点。

让我们比较一下方法。

基于示例的测试与基于属性的测试

让我们举一个power函数的例子:

function<int(int, int)> power = [](auto first, auto second){
    return pow(first, second);
};

您将如何使用基于示例的测试来测试它?我们需要为第一个和第二个找出一些有趣的值,并将它们组合起来。为了这个练习的目的,我们将把自己限制在正整数上。一般来说,整数的有趣值是— 01、多和最大。这导致以下可能的情况:

  • 0 0 - >未定义(* c++ 中的pow实现返回1,除非启用了特定错误)
  • 00 到最大值之间的任意整数 - > 0
  • 1 任意整数 - > 1
  • (除 0 以外的任意整数) 0 - > 1
  • 2 2 - > 4
  • 2 不溢出 - >待计算值的最大整数
  • 10 5 - > 100000
  • 10 不溢出的最大整数 - >待计算值

这个列表并不完整,但它展示了对问题的有趣分析。所以,让我们写这些测试:

TEST_CASE("Power"){
    int maxInt = numeric_limits<int>::max();
    CHECK_EQ(1, power(0, 0));
    CHECK_EQ(0, power(0, 1));
    CHECK_EQ(0, power(0, maxInt));
    CHECK_EQ(1, power(1, 1));
    CHECK_EQ(1, power(1, 2));
    CHECK_EQ(1, power(1, maxInt));
    CHECK_EQ(1, power(2, 0));
    CHECK_EQ(2, power(2, 1));
    CHECK_EQ(4, power(2, 2));
    CHECK_EQ(maxInt, power(2, 31) - 1);
    CHECK_EQ(1, power(3, 0));
    CHECK_EQ(3, power(3, 1));
    CHECK_EQ(9, power(3, 2));
    CHECK_EQ(1, power(maxInt, 0));
    CHECK_EQ(maxInt, power(maxInt, 1));
}

这显然不是我们需要检查以确定幂函数起作用的完整测试列表,但这是一个良好的开端。看着这个列表,我在想,你认为——你会写更多还是更少的测试?我肯定想写更多,但我在这个过程中失去了动力。当然,问题之一是我在代码之后编写了这些测试;我更有动力将它们和代码一起编写,就像在测试驱动开发 ( TDD )中一样。但也许有更好的方法?

让我们换个角度思考一下。我们是否可以测试某些或所有预期输出的属性?让我们写一个清单:

  • 0 0 - >未定义(C++ 中幂函数默认为 1)
  • 0【1.. - > 0
  • 值:[1.. 0 - > 1
  • 值:[0.. 1 - >值

这些都是一些明显的性质。然而,它们只覆盖了一小部分值。我们还是需要涵盖xT5y的一般情况,其中 xy 既不是0也不是1。我们能在这里找到任何财产吗?好吧,想想整数幂的数学定义——它是一个重复的乘法。因此,对于任何大于1xy 值,我们可以推断如下:

我们这里确实有一个边界问题,因为计算可能会溢出。所以需要选取 xy 的值,使得 x y 小于maxInt。处理这个问题的一种方法是先挑 x ,在 y=2maxy=floor(log<sub>x</sub>maxInt)之间挑 y 。为了使它尽可能接近边界,我们应该总是选择maxy作为一个值。为了检查溢出情况,我们只需要测试 xmaxy + 1的幂溢出。

当然,前面的方法意味着我们信任标准库中对数函数的结果。如果你的测试偏执狂比我的大,我建议对从2maxInt的所有基数和数值maxInt使用经过验证的对数表。然而,我将使用 STL 对数函数。

我们现在有了幂函数的数学性质的列表。但是我们希望像前面看到的那样,有间隔地实现它们。我们能做到吗?输入数据生成器。

发电机

生成器是函数式编程语言的主要特征。它们通常通过 lambdas 和 lazy 求值的组合来实现,允许如下代码:

// pseudocode
vector<int> values = generate(1, maxInt, [](){/*generatorCode*/}).pick(100)

生成器函数通常生成无限数量的值,但是因为它是惰性求值的,所以100值只有在调用pick时才会具体化。

C++ 还没有对延迟求值和数据生成器的标准支持,所以我们必须实现自己的生成器。值得注意的是,C++ 20 已经在标准中采用了令人敬畏的范围库,它支持这两个特性。为了本章的目标,我们将坚持目前可用的标准,但是在本书的最后几章中,您将会发现 ranges library 的基本用法。

首先,我们如何生成数据?STL 通过使用uniform_int_distribution类为我们提供了一个生成均匀分布的随机整数的好方法。我们先来看看代码;我添加了注释来解释发生了什么:

auto generate_ints = [](const int min, const int max){
    random_device rd; // use for generating the seed
    mt19937 generator(rd()); // used for generating pseudo-random 
        numbers
    uniform_int_distribution<int> distribution(min, max); // used to 
        generate uniformly distributed numbers between min and max
    auto values = transformAll<vector<int>>(range(0, 98), // generates 
        the range [0..98]
            [&distribution, &generator](auto){
                return distribution(generator); // generate the random 
                    numbers
            });
    values.push_back(min); // ensure that min and max values are 
        included
    values.push_back(max);
    return values;
};

该功能将生成从minmax的均匀分布的数字。我更喜欢总是包括区间的边缘,因为这些对于测试来说总是有趣的值。

我们还使用了一个名为range的函数,你还没有看到。它的目标是用从minValuemaxValue的值填充向量,以允许更简单的转换。这是:

auto range = [](const int minValue, const int maxValue){
    vector<int> range(maxValue - minValue + 1);
    iota(range.begin(), range.end(), minValue);
    return range;
};

值得注意的是,在函数式编程语言中,范围通常是延迟计算的,这大大减少了它们的内存占用。尽管对于我们例子的目标来说,这很好。

前面的generator函数允许我们为测试创建输入数据,均匀分布在 1 和最大整数值之间。只需要一个简单的绑定:

auto generate_ints_greater_than_1 = bind(generate_ints, 1, numeric_limits<int>::max());

让我们将它用于基于属性的测试。

对属性进行测试

让我们再次查看要检查的属性列表:

  • 0 0 - >未定义(C++ 中幂函数默认为 1)
  • 0【1.. - > 0
  • 值:[1.. 0 - > 1
  • 值:[0.. 1 - >值
  • xy= xy-1 x*

我们现在将依次实现每个属性。对于每一个属性,我们将使用普通的基于示例的测试,或者由generate_ints_greater_than_1函数启发的数据生成器。让我们从最简单的属性开始——*00*应该是未定义的——或者实际上是其标准实现中的1

属性:00 ->未定义

第一个是非常简单的实现,使用一个普通的基于示例的测试。为了保持一致,我们将在函数中提取它:

auto property_0_to_power_0_is_1 = [](){
    return power(0, 0) == 1;
};

在我们的测试中,我们还将编写属性的描述,以便获得信息输出:

TEST_CASE("Properties"){
    cout << "Property: 0 to power 0 is 1" << endl;
    CHECK(property_0_to_power_0_is_1);
 }

运行时,这将导致以下输出通过测试:

g++ -std=c++ 17 propertyBasedTests.cpp -o out/propertyBasedTests
./out/propertyBasedTests
[doctest] doctest version is "2.0.1"
[doctest] run with "--help" for options
Property: 0 to power 0 is 1
===============================================================================
[doctest] test cases:      1 |      1 passed |      0 failed |      0 skipped
[doctest] assertions:      1 |      1 passed |      0 failed |
[doctest] Status: SUCCESS!

这已经够简单的了!我们现在有了基于属性的测试的基本结构。下一个测试需要一个数据生成器,但是我们已经有了。让我们看看除了0等于0之外,它对任何力量的0属性如何起作用。

属性:0[1..maxInt] -> 0

我们需要从1maxInt的数字生成器,我们已经实现了。然后我们需要一个属性函数来检查从1maxInt的任何指数,0上升到指数等于0。代码很容易写:

auto prop_0_to_any_nonzero_int_is_0= [](const int exponent){
    CHECK(exponent > 0); // checking the contract just to be sure
    return power(0, exponent) == 0;
};

接下来,我们需要检查这个属性。由于我们有一个生成值的列表,我们可以使用all_of函数根据属性检查所有的值。为了提供更多信息,我决定显示我们正在使用的值列表:

auto printGeneratedValues = [](const string& generatorName, const auto& 
    values){
        cout << "Check generator " << generatorName << endl;
        for_each(values.begin(), values.end(), [](auto value) { cout << 
            value << ", ";});
        cout << endl;
 };

auto check_property = [](const auto& generator, const auto& property, const string& generatorName){
    auto values = generator();
    printGeneratedValues(generatorName, values);
    CHECK(all_of_collection(values, property));
};

最后,我们可以写我们的测试。我们再次在测试前显示属性名:

TEST_CASE("Properties"){
    cout << "Property: 0 to power 0 is 1" << endl;
    CHECK(property_0_to_power_0_is_1);

    cout << "Property: 0 to [1..maxInt] is 0" << endl;
    check_property(generate_ints_greater_than_1,  
        prop_0_to_any_nonzero_int_is_0, "generate ints");
}

运行测试会产生以下输出:

Property: 0 to power 0 is 1
Property: 0 to [1..maxInt] is 0
Check generator generate ints
1073496375, 263661517, 1090774655, 590994005, 168796979, 1988143371, 1411998804, 1276384966, 252406124, 111200955, 775255151, 1669887756, 1426286501, 1264685577, 1409478643, 944131269, 1688339800, 192256171, 1406363728, 1624573054, 2654328, 1025851283, 1113062216, 1099035394, 624703362, 1523770105, 1243308926, 104279226, 1330992269, 1964576789, 789398651, 453897783, 1041935696, 561917028, 1379973023, 643316376, 1983422999, 1559294692, 2097139875, 384327588, 867142643, 1394240860, 2137873266, 2103542389, 1385608621, 2058924659, 1092474161, 1071910908, 1041001035, 582615293, 1911217125, 1383545491, 410712068, 1161330888, 1939114509, 1395243657, 427165959, 28574042, 1391025789, 224683120, 1222884936, 523039771, 1539230457, 2114587312, 2069325876, 166181790, 1504124934, 1817094271, 328329837, 442231460, 2123558414, 411757963, 1883062671, 1529993763, 1645210705, 866071861, 305821973, 1015936684, 2081548159, 1216448456, 2032167679, 351064479, 1818390045, 858994762, 2073835547, 755252854, 2010595753, 1882881401, 741339006, 1080861523, 1845108795, 362033992, 680848942, 728181713, 1252227588, 125901168, 1212171311, 2110298117, 946911655, 1, 2147483647, 
===============================================================================
[doctest] test cases:      1 |      1 passed |      0 failed |      0 skipped
[doctest] assertions:    103 |    103 passed |      0 failed |
[doctest] Status: SUCCESS!

可以看到,测试使用了一堆随机值,最后两个值是1maxInt

是时候停下来反思一下了。这些测试不寻常。单元测试的关键思想之一是拥有可重复的测试,但是在这里,我们有一堆随机的值。这些算吗?当一种价值观导致失败时,我们该怎么办?

这些都是很棒的问题!首先,使用基于属性的测试并不排除基于示例的测试。事实上,我们现在正在混合这两者——*00*是一个例子而不是一个属性。所以,当有意义的时候,不要犹豫检查任何特定的值。

其次,支持基于属性的测试的库允许收集特定的失败值,并自动对这些失败值进行重新测试。这很简单——每当出现故障时,将值保存在某个地方,并在测试运行时将它们包含在下一代中。这不仅可以让您更彻底地测试,还可以发现代码的行为。

因此,我们必须将基于示例的测试和基于属性的测试视为互补技术。第一个帮助你使用测试驱动开发 ( TDD )驱动代码,查看有趣的案例。第二个可以让你找到你没有考虑过的案例,重新测试同样的错误。两者都有用,只是方式不同。

那么让我们继续写我们的属性。下一个大约是0等于1的幂的任意数。

属性:值:[1..最大]0 -> 1

我们已经准备好了一切,我们只需要写下来:

auto prop_anyIntToPower0Is1 = [](const int base){
    CHECK(base > 0);
    return power(base, 0) == 1;
};

测试结果如下:

TEST_CASE("Properties"){
    cout << "Property: 0 to power 0 is 1" << endl;
    CHECK(property_0_to_power_0_is_1);

    cout << "Property: 0 to [1..maxInt] is 0" << endl;
    check_property(generate_ints_greater_than_1, 
        prop_0_to_any_nonzero_int_is_0, "generate ints");

    cout << "Property: any int to power 0 is 1" << endl;
    check_property(generate_ints_greater_than_1, 
        prop_anyIntToPower0Is1, "generate ints");
}

运行测试会产生以下输出(为简洁起见,省略了几行):

Property: 0 to power 0 is 1
Check generator generate ints
1673741664, 1132665648, 342304077, 936735303, 917238554, 1081591838, 743969276, 1981329112, 127389617, 
...
 1, 2147483647, 
Property: any int to power 0 is 1
Check generator generate ints
736268029, 1304281720, 416541658, 2060514167, 1695305196, 1479818034, 699224013, 1309218505, 302388654, 765083344, 430385474, 648548788, 1986457895, 794974983, 1797109305, 1131764785, 1221836230, 802640954,
...
1543181200, 1, 2147483647, 
===============================================================================
[doctest] test cases:      1 |      1 passed |      0 failed |      0 skipped
[doctest] assertions:    205 |    205 passed |      0 failed |
[doctest] Status: SUCCESS!

从前面的例子可以看出,这些数字确实是随机的,同时总是包括1maxInt

我们掌握了窍门!下一个性质是1的幂的任何值都是值。

属性:值:[0..最大值]1 ->值

我们需要另一个生成器方法,从0开始。我们只需要再次使用绑定魔法来获得所需的结果:

auto generate_ints_greater_than_0 = bind(generate_ints, 0, numeric_limits<int>::max());

这个属性很容易写:

auto prop_any_int_to_power_1_is_the_value = [](const int base){
    return power(base, 1) == base;
};

考验显而易见:

TEST_CASE("Properties"){
    cout << "Property: 0 to power 0 is 1" << endl;
    CHECK(property_0_to_power_0_is_1);

    cout << "Property: 0 to any non-zero power is 0" << endl;
    check_property(generate_ints_greater_than_1, 
        prop_0_to_any_nonzero_int_is_0, "generate ints");

    cout << "Property: any int to power 0 is 1" << endl;
    check_property(generate_ints_greater_than_1, 
        prop_anyIntToPower0Is1, "generate ints");

    cout << "Property: any int to power 1 is the value" << endl;
    check_property(generate_ints_greater_than_0, 
        prop_any_int_to_power_1_is_the_value, "generate ints");
}

运行测试再次导致通过。

让我们花点时间再思考一下:

  • 我们检查多少值?答案是301
  • 有多少行测试代码?测试代码只有 23 行代码,而我们在测试中重用的函数大约有 40 行代码。

这不是很神奇吗?这难道不是你测试的一项有价值的投资吗?

我们知道怎么做。现在是我们练习中最复杂的性质的时候了——任何升到幂的数 y 等于升到幂的数 y-1 乘以这个数。

属性:xy = xy-1 * x

这将需要我们生成两组值, xy ,这样xyT17】最大。我花了一些时间摆弄数据发生器,但我发现任何大于x 只能测试 y=1 。因此,我将使用两台发电机;第一个将生成介于2之间的数字,而第二个将生成大于且小于maxInt的数字:

auto generate_ints_greater_than_2_less_sqrt_maxInt = bind(generate_ints, 2, sqrt(numeric_limits<int>::max()));

属性的第一部分如下:

cout << "Property: next power of x is previous power of x multiplied by  
    x" << endl;
check_property(generate_ints_greater_than_2_less_sqrt_maxInt, 
    prop_nextPowerOfXIsPreviousPowerOfXMultipliedByX, "generate greater 
        than 2 and less than sqrt of maxInt");

为了实现这个属性,我们还需要为x基数生成指数,这样我们就可以把这个属性写成如下:

auto prop_nextPowerOfXIsPreviousPowerOfXMultipliedByX = [](const int x){
    auto exponents = bind(generate_exponent_less_than_log_maxInt, x);
    return check_property(exponents, [x](auto y){ return power(x, y) ==  
      power(x, y - 1) * x;}, "generate exponents for " + to_string(x));
};

从生成器函数的名称可以看出,我们需要生成1log x maxInt 之间的数字。计算 x y 时,任何高于该值的数字都会溢出。由于 STL 中没有通用的对数函数,我们需要实现一个。要计算 log x maxInt ,我们只需要使用一个数学等式:

auto logMaxIntBaseX = [](const int x) -> int{
    auto maxInt = numeric_limits<int>::max() ;
    return floor(log(maxInt) / log(x));
};

我们的生成器函数如下:

auto generate_exponent_less_than_log_maxInt = [](const int x){
    return generate_ints(1, logMaxIntBaseX(x));
};

有了这些,我们就可以进行测试了。以下是输出的简短部分:

Check generator generate exponents for 43740
1, 2, 
Check generator generate exponents for 9320
1, 2, 
Check generator generate exponents for 2
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 
Check generator generate exponents for 46340
1, 2,

测试的最后一部分是将 + 1 到maxInt的间隔相加:

check_property(generate_ints_greater_than_sqrt_maxInt,  
    prop_nextPowerOfXIsPreviousPowerOfXMultipliedByX, "generate greater    
    than sqrt of maxInt");

这也导致生成函数的更新,以支持一些边缘情况;有关以下代码中的说明,请参考注释:

auto generate_ints = [](const int min, const int max){
    if(min > max) { // when lower range is larger than upper range, 
        just return empty vector
            return vector<int>();
    }
    if(min == max){ // if min and max are equal, just return {min}
        return range(min, min);
    }

    if(max - min <= 100){ // if there not enough int values in the 
        range, just return it fully
            return range(min, max);
    }
    ...
}

就这样,我们实现了我们的最终财产!

结论

我们现在只需几行代码就可以检查以下所有内容:

  • 0 0 - >未定义(C++ 中幂函数默认为 1)
  • 0【1.. - > 0
  • 值:[1.. 0 - > 1
  • 值:[0.. 1 - >值
  • xy= xy-1 x*

这与更常用的基于示例的测试方法相比如何?我们用更少的代码进行更多的测试。我们可以在代码中发现隐藏的问题。但是属性比示例更难识别。我们还发现,基于属性的测试与基于示例的测试配合得非常好。

所以,现在让我们解决寻找属性的问题。这需要一些分析,我们将探索一种实用的方法,您可以通过数据驱动测试从示例中演化属性。

从示例到数据驱动测试再到属性

当我第一次听说基于属性的测试时,我有两个问题。首先,我认为它们是为了取代示例测试——我们现在知道它们不是;只是并排使用这两种技术。第二,我不知道如何想出好的属性。

然而,我有一个好主意,如何提供好的例子,如何消除测试之间的重复。我们已经看到了一个关于如何给出幂函数的好例子的例子;让我们回顾一下:

  • *0 0 - >未定义(除非启用特定错误,否则 C++ 中的 pow 实现返回 1)
  • 00 到最大值之间的任意整数 - > 0
  • 1 任意整数 - > 1
  • (除 0 以外的任意整数) 0 - > 1
  • 2 2 - > 4
  • 2 不溢出的最大 int->待计算值
  • 10 5 - > 100000
  • 10 不溢出的最大 int->待计算值

我们还看到,为这些情况编写基于示例的测试非常容易:

TEST_CASE("Power"){
    int maxInt = numeric_limits<int>::max();
    CHECK_EQ(1, power(0, 0));
    CHECK_EQ(0, power(0, 1));
    CHECK_EQ(0, power(0, maxInt));
    CHECK_EQ(1, power(1, 1));
    CHECK_EQ(1, power(1, 2));
    CHECK_EQ(1, power(1, maxInt));
    CHECK_EQ(1, power(2, 0));
    CHECK_EQ(2, power(2, 1));
    CHECK_EQ(4, power(2, 2));
    CHECK_EQ(maxInt, power(2, 31) - 1);
    CHECK_EQ(1, power(3, 0));
    CHECK_EQ(3, power(3, 1));
    CHECK_EQ(9, power(3, 2));
    CHECK_EQ(1, power(maxInt, 0));
    CHECK_EQ(maxInt, power(maxInt, 1));
}

这些例子展示了代码的相似性。0123碱基重复多次。我们在第 9 章功能编程的测试驱动开发中看到,我们可以通过指定多个输入值来消除数据驱动测试的这种相似性:

TEST_CASE("1 raised to a power is 1"){
    int exponent;

    SUBCASE("0"){
        exponent = 0;
    }
    SUBCASE("1"){
        exponent = 1;
    }
    SUBCASE("2"){
        exponent = 1;
    }
    SUBCASE("maxInt"){
        exponent = maxInt;
    }

    CAPTURE(exponent);
    CHECK_EQ(1, power(1, exponent));
}

在我努力消除这些相似性一段时间后,我开始看到属性。很明显,在这种情况下,我们可以添加一个测试,通过使用随机输入而不是特定的例子来检查相同的数学属性。事实上,我们在上一节中写了它,它看起来是这样的:

cout << "Property: any int to power 1 is the value" << endl;
check_property(generate_ints_greater_than_0, 
    prop_any_int_to_power_1_is_the_value, "generate ints");

所以我的建议是——如果你对这个问题反思几分钟,找到要检查的数学属性,那就太好了!(编写基于属性的测试,并添加尽可能多的基于示例的测试,以确信您已经涵盖了这些情况。)看不到的话,不用担心;继续添加基于示例的测试,通过使用数据驱动的测试来消除测试之间的重复,最终您将揭示属性。然后,添加基于属性的测试,并决定如何处理现有的基于示例的测试。

好的属性,坏的属性

因为属性是比示例更高的抽象层次,所以很容易以混乱或不清楚的方式实现它们。您已经需要非常注意基于示例的测试;现在,您需要加强与基于属性的测试相关的工作。

首先,好的属性就像好的单元测试。因此,我们希望拥有如下特性:

  • 小的
  • 恰当而明确地命名
  • 当他们失败时给出一个非常明确的信息
  • 快的
  • 可重复的

但是基于属性的测试有一个警告——既然我们使用的是随机值,难道我们不应该期待随机失败吗?当基于属性的测试失败时,我们会学到一些关于代码的新东西,所以这是值得庆祝的。然而,我们应该期望到 2010 年久而久之的失败会更少,我们应该消除我们的缺陷。如果您的基于属性的测试每天都失败,那么肯定有问题——可能是属性太大,或者实现有很多漏洞。如果您的基于属性的测试不时失败,并且它们在代码中显示出一个可能的错误——这很好。

基于属性的测试的难点之一是保持生成器和属性检查没有错误。这也是代码,任何代码都可能有 bug。在基于示例的测试中,我们通过将单元测试简化到几乎不可能出错的程度来处理这个问题。请注意,属性更复杂,因此可能需要更多的关注。旧的原则保持简单,愚蠢的在基于属性的测试中更有价值。因此,比起更大的属性,更喜欢更小的属性,进行您的分析,并与同事一起检查您的代码——包括名称和实现。

关于执行的一些话

在本章中,我们使用了一组自定义函数来实现数据生成器,以保持代码标准 C++ 17。然而,这些功能是为学习该技术而优化的,还没有做好生产准备。您可能已经看到,它们没有针对内存占用或性能进行优化。我们已经可以通过巧妙使用迭代器让它们变得更好,但是还有更好的方法。

如果您可以使用范围库或者使用 C++ 20 编译您的测试,那么实现无限数据生成器是非常容易的(由于延迟求值)。我还建议您研究基于属性的测试库,或者生成器库,因为有些生成器已经被其他人编写过了,一旦您理解了这个概念,在代码中使用它们会快得多。

摘要

基于属性的测试是对我们已经知道并使用多年的基于示例的测试的一个受欢迎的补充。它们向我们展示了如何将数据生成和一些分析结合起来,以消除测试中的重复,并找到我们没有考虑到的案例。

基于属性的测试是由数据生成器实现的,使用纯函数非常容易实现。随着 C++ 20 或范围库的出现,延迟求值将变得更加容易。

但是基于属性的测试的核心技术是识别属性。我们已经看到了两种方法——第一种方法是分析示例,第二种方法是编写基于示例的测试,消除重复以将其转换为数据驱动的测试,然后用属性替换数据行。

最后,请记住,基于属性的测试是代码,它们需要非常干净、易于更改和易于理解。尽可能偏爱小属性,并通过明确命名使它们非常容易理解。

在下一章中,我们将研究如何使用纯函数来支持我们的重构工作,以及如何将设计模式实现为函数。