当前位置: 首页 > news >正文

CppCon 2015 学习:RapidCheck Property based testing for C++

也是介绍一个什么库

RapidCheck 是一个用于 C++基于属性的测试库。它帮助开发人员自动化地生成输入数据,并且验证这些数据是否满足预定的属性或条件。与传统的单元测试不同,属性测试关注的是验证程序的行为,而不是检查具体的输出值。

基本概念:

  • 属性测试(Property-Based Testing):通过定义一组属性(如某些条件或规则)来描述程序的预期行为。测试框架将自动生成大量的随机数据,并验证这些数据是否能满足属性要求。
    例如,假设我们有一个排序函数,它的属性应该是“排序后的数组应该是非递减的”。
  • 快速生成测试数据RapidCheck 会自动生成各种随机数据,并测试这些数据是否符合预期的属性。这些数据往往包括边界情况、极端情况,甚至是常规情况。
  • 自动化失败案例:如果某个属性未通过,RapidCheck 会尽量缩小失败输入的范围,并给出最小的失败示例,帮助开发人员快速定位问题。

如何使用 RapidCheck

  1. 安装
    • 使用 RapidCheck 需要首先将其集成到项目中,可以通过源代码方式或通过包管理器(例如 vcpkg)来安装。
  2. 编写属性测试
    • 属性测试一般定义一个符合条件的规则,然后让框架通过生成随机数据来验证这个规则。例如,我们希望验证一个排序算法:
    #include <rapidcheck.h>
    #include <vector>
    #include <algorithm>
    bool is_sorted(const std::vector<int>& vec) {return std::is_sorted(vec.begin(), vec.end());
    }
    // 定义属性
    rc::prop("Sorting a vector should result in a sorted vector", [](const std::vector<int>& vec) {std::vector<int> copy = vec;   // 复制数据std::sort(copy.begin(), copy.end());  // 排序return is_sorted(copy);   // 验证是否排序正确
    });
    
    以上代码定义了一个属性:“排序一个数组应该得到一个已排序的数组”。RapidCheck 会生成随机的整数数组并自动验证这个属性。
  3. 运行测试
    • 使用 rapidcheck::check 或其他命令运行这些属性测试,RapidCheck 会尝试各种随机的输入数据来测试这些属性,并报告任何不符合条件的情况。
    rc::check("Sorting a vector should result in a sorted vector", [](const std::vector<int>& vec) {std::vector<int> copy = vec;std::sort(copy.begin(), copy.end());return is_sorted(copy);
    });
    
  4. 测试失败时的自动简化
    • 如果某个测试失败,RapidCheck 会自动简化失败的输入数据,找到导致问题的最小示例,以便快速定位问题。

优势

  • 高效的边界测试RapidCheck 自动生成各种边界数据、随机数据,确保代码在极端情况下也能正常工作。
  • 更少的样本,更多的覆盖:通过属性测试,可以避免手动编写大量不同的测试用例,且覆盖范围较大。
  • 快速定位问题:失败时提供最小的输入案例,帮助快速定位问题。

总结

RapidCheck 是一种高效的、基于属性的测试方法,它通过生成随机数据来验证代码是否符合预定的行为特性。它不仅可以发现边界情况和极端输入,还能帮助开发者快速定位错误,非常适合用于验证复杂逻辑和算法的正确性。
这段代码是一个典型的 单元测试(Unit Testing)示例,使用了 C++ 测试框架 Catch2。它用来验证一个函数 concat 的功能是否符合预期。我们来逐行分析:

代码分析:

TEST_CASE("concatenates two strings") {const auto s = concat("foo", "bar");REQUIRE(s == "foobar");
}
  1. TEST_CASE("concatenates two strings")
    • 这是 Catch2 测试框架的一个宏,用于定义一个测试用例。
    • "concatenates two strings" 是这个测试用例的描述,帮助我们理解这个测试的目的。
    • TEST_CASE 的作用是将一个特定的测试逻辑封装成一个单元,以便于运行时进行执行。
  2. const auto s = concat("foo", "bar");
    • 这里调用了一个假设存在的函数 concat,传入两个字符串 "foo""bar"
    • concat 的作用应该是将这两个字符串连接起来(即 “foo” 和 “bar” 连接为 “foobar”)。
    • const auto s 使用了 auto 关键字,表示 s 的类型是由编译器自动推导的,假设 concat 返回的是一个 std::string 类型,所以 s 是一个字符串。
  3. REQUIRE(s == "foobar");
    • REQUIRE 是 Catch2 框架提供的断言宏。它的作用是检查一个条件是否为真。
    • 在这个例子中,它检查 s(即 concat("foo", "bar") 的结果)是否等于 "foobar"
    • 如果条件不成立(即 s 不等于 "foobar"),测试会失败,且框架会报告错误。
    • 如果条件成立,测试会通过,且不做任何报告。

测试逻辑

  • 测试的目的是验证 concat 函数的正确性。具体来说,测试它是否能够正确地将两个字符串 “foo” 和 “bar” 连接成一个字符串 “foobar”。
  • REQUIRE 断言确保了 concat("foo", "bar") 返回的结果是 "foobar",如果不是,测试就会失败。

总结

这段代码是一个简单的单元测试示例,目的是确保 concat 函数正确地将两个字符串连接成一个新的字符串。测试框架会自动运行这个测试,并根据 REQUIRE 的条件判断测试是否通过。如果通过,测试成功;如果失败,测试框架会给出详细的错误信息。
这种单元测试方法在软件开发中非常常见,用来验证小的功能单元(如函数)是否按照预期工作。

这段代码是一个单元测试的例子,使用的是 C++ 测试框架 Catch2。它验证了一个 concat 函数是否能正确地将两个字符串连接起来。我们来逐行分析:

代码分析:

TEST_CASE("given 'foo' and 'bar',"" yields 'foobar'") { const auto s = concat("foo", "bar"); REQUIRE(s == "foobar"); 
}
  1. TEST_CASE("given 'foo' and 'bar'," " yields 'foobar'")
    • 这是 Catch2 测试框架的 TEST_CASE 宏,用于定义一个测试用例。
    • "given 'foo' and 'bar', yields 'foobar'" 是该测试用例的描述文本。
      • 测试用例描述了输入是两个字符串 'foo''bar',期望的输出是 'foobar'
      • 注意,字符串的分割是允许的,两个字符串会连接成一个完整的描述:"given 'foo' and 'bar', yields 'foobar'"
  2. const auto s = concat("foo", "bar");
    • 这行代码调用了一个函数 concat,传入两个字符串 "foo""bar"
    • concat 函数的目标是将这两个字符串连接在一起,假设它返回一个 std::string,因此 s 会保存这个连接后的字符串。
    • 使用 auto 关键字,编译器会推断出 s 的类型,这里推断为 std::string
  3. REQUIRE(s == "foobar");
    • REQUIRE 是 Catch2 提供的一个断言宏,用于验证给定的条件是否为真。
    • 在此例中,它检查 s 的值是否等于 "foobar",即检查 concat("foo", "bar") 是否正确返回 "foobar"
    • 如果 s == "foobar" 为假,测试会失败,并且测试框架会报告错误。

测试逻辑

  • 测试用例的目的是验证 concat 函数的行为,即它能否正确地将两个输入字符串 "foo""bar" 连接成 "foobar"
  • 如果 s 的值不是 "foobar",测试就会失败,框架会输出错误信息;如果值相等,测试成功。

总结

这段代码定义了一个简单的单元测试,目的是验证 concat 函数将 "foo""bar" 合并成 "foobar" 是否正常工作。测试框架会自动运行该测试,并判断 REQUIRE 的条件是否成立。

  • 测试用例的描述 "given 'foo' and 'bar', yields 'foobar'" 清楚地说明了输入和期望的输出。
  • 这种写法使得测试用例更具可读性和自文档化性质,任何人查看时都能清楚地知道测试的目标和期望行为。
    这种单元测试方法广泛应用于软件开发中,帮助开发者确保函数的正确性,并可以方便地自动化运行和验证。

这段代码展示了如何通过属性测试来验证 concat 函数的行为。让我们逐行分析并解释其背后的逻辑。

背景

你要验证的属性是:给定两个输入字符串 ab,经过 concat(a, b) 函数的连接后,返回的字符串 c 应该满足以下条件:

  1. ca 开头
  2. cb 结尾
  3. c.size() 等于 a.size() + b.size()
    这个属性可以作为一个测试条件来验证 concat 函数的正确性。

代码分析

bool property(const std::string &a, const std::string &b) { const auto c = concat(a, b); return c.size() == a.size() + b.size(); 
}
  1. bool property(const std::string &a, const std::string &b)
    • 这个函数接受两个 std::string 类型的参数 ab,返回一个 bool 类型的值,表示给定的属性是否成立。
    • 这是一个检查属性的函数,它验证 concat(a, b) 是否满足我们设定的条件。
  2. const auto c = concat(a, b);
    • 这里调用了 concat 函数,将 ab 作为参数传入,并将返回的结果存储在变量 c 中。
    • cstd::string 类型,它保存了两个字符串 ab 连接后的结果。
  3. return c.size() == a.size() + b.size();
    • 这行代码检查 c 的大小是否等于 ab 的大小之和,即:c.size() 是否等于 a.size() + b.size()
    • 如果条件成立,说明 concat 函数正确地连接了两个字符串,否则返回 false

属性的含义

在属性测试中,我们定义了一个 属性,它描述了 concat 函数的期望行为。这个属性包括:

  • c 必须具有 ab 的总长度:c.size() == a.size() + b.size()
  • 通过这个属性,我们可以对 concat 函数进行验证,确保它在不同输入情况下的行为是符合预期的。

如何使用

你可以通过将 property 函数与不同的字符串 ab 配合使用,进行多次测试。例如:

TEST_CASE("concat returns correct size") {REQUIRE(property("foo", "bar"));  // Should return trueREQUIRE(property("hello", "world"));  // Should return trueREQUIRE(property("", "empty"));  // Should return trueREQUIRE(property("abc", ""));  // Should return true
}

通过这种方式,你能够自动化地测试 concat 函数,确保它始终返回正确大小的连接字符串。

总结

  • 这段代码定义了一个属性测试函数 property,用于验证 concat 函数是否能按预期连接两个字符串并返回正确的字符串长度。
  • 属性测试是一种有力的测试方法,可以帮助你在多个输入上确保函数行为一致,并捕捉潜在的错误。

如何让我们信服?

  • 尝试随机的东西:可以通过随机输入来测试程序的行为,这是一个探索性的测试方式。
  • 折中的方法:介于全面测试(exhaustive testing)和“我能忍受写多少就写多少”之间的一种平衡。
  • 确实有效:这种方法是有效的,能够帮助你发现潜在的 bug。
QuickCheck
  • QuickCheck 是一个轻量级的工具,最早用于 Haskell 程序的随机测试(Koen Claessen 和 John Hughes,ICFP 2000)。
  • QuickCheck 的一个重要示例代码:
prop_concatsize a b = length (concat a b) == length a + length b
  • 这个例子展示了如何通过属性测试来确保 concat 函数的行为正确:连接两个字符串后,其长度应等于两个字符串长度的总和。
我创建了 RapidCheck
  • RapidCheck 是我基于 Haskell/Erlang 中 QuickCheck 的基本概念所创建的,感谢 Hughes 和 Claessen 的贡献。
  • 特点
    • 非常少的样板代码(boilerplate)。
    • 功能丰富,包括:
      • 多种生成器(generators)和组合器(combinators)。
      • 测试用例收缩(test case shrinking)。
      • 有状态的测试框架(stateful testing framework)。
RapidCheck 的属性测试
  • RapidCheck 中,可以定义类似于 Haskell 中的属性:
bool property(const std::string &a, const std::string &b) { const auto c = concat(a, b); return c.size() == a.size() + b.size(); 
}

这个 property 函数和 Haskell 示例类似,验证 concat 函数的输出字符串大小是否正确。

使用 RapidCheck 运行测试
  • 使用 RapidCheck 来运行测试非常简单:
rc::check(&property);

或者可以直接传递一个 lambda 函数:

rc::check([](const std::string &a, const std::string &b) { const auto c = concat(a, b); return c.size() == a.size() + b.size(); 
});
测试结果
  • Falsifiable after 21 tests and 17 shrinks
    • 经过 21 次测试和 17 次收缩后,发现测试失败。
    • 错误的输入是一个 std::tuple<std::string, std::string> 类型,其中的值是:
      • ("", "aaaaaaaaaaaaaaaa")
        这里的意思是,当 concat 被输入一个空字符串和一个较长的字符串时,它没有按预期工作。

总结:

  • RapidCheck 是一个基于属性的随机测试工具,它借鉴了 Haskell 中 QuickCheck 的思想。通过定义属性(如 concat(a, b) 的大小属性)来验证代码的正确性。
  • 通过这种方法,你能够发现测试用例中不易察觉的潜在错误,并且不需要编写大量的测试代码。

Shrinking(收缩)

  • 在属性测试中,“收缩”是一个重要概念。它指的是在测试过程中,当发现某些输入导致错误时,工具会不断地尝试简化输入数据,以便找出 最小 的触发错误的输入。这有助于开发者更快定位并修复问题。
  • 你提供的数字列表经过收缩处理后,从最初复杂的情况缩减到了 77567,然后进一步缩小到 65536,这意味着该测试用例是通过某个非常特定的、最小的输入数据触发的。
复杂案例 vs. 简单案例
  • 复杂案例(Complex case):原始输入数据看起来是很大的数据集合,包含了多种数字。测试系统对这些输入数据进行了多次“收缩”,最终找到了一个 最小的输入,即 77567,然后又缩小到 65536。通过这种方式,可以最小化问题并加速调试过程。
  • 最小案例(Minimal case):这个过程是测试的精髓,因为它能帮助开发者关注到具体触发错误的最小条件,从而避免在庞大的数据中迷失方向。
属性测试的优势
  1. 能够发现你未曾考虑到的 bug:由于属性测试依赖随机生成的输入数据,它能覆盖到你可能没有考虑到的边界情况和特定条件,从而发现潜在的 bug。
  2. 少量代码实现更多覆盖:通过定义一些简单的属性测试,你可以获得比传统单元测试更多的测试覆盖,且测试代码量更少。
  3. 最小反例:当发现错误时,属性测试会给出最小的反例。这意味着你只需关注一小段数据,而不需要处理庞大的测试输入,这可以大大提高调试效率。
  4. 帮助你思考代码的目的,而不是行为:属性测试让你专注于代码应该做什么,而不是它现在具体做了什么。这有助于你在设计时从更高层次考虑程序的正确性和稳定性。

总结:

  • 收缩(Shrinking) 是属性测试中的一个关键功能,它帮助开发者从复杂的输入数据中找到最小的错误触发条件。
  • 属性测试的优势在于它能够覆盖到更多的情况,发现更多潜在的 bug,而且通过最小反例,调试更加高效。它促使开发者思考代码的意图和目的,而不仅仅是当前的行为。

RapidCheck的生成器

RapidCheck 中,所有生成的数据都来自 生成器。生成器用于为测试生成输入数据,是支持自定义类型的关键点。

内置支持的类型

RapidCheck 支持多种常见的数据类型和容器:

  • 基本数据类型:如整型、浮动点数等
  • 标准容器:如 std::array<T, N>, std::vector<T>, std::deque<T>, std::list<T>, std::set<T>, std::map<K, V>, 等等
  • 其他类型
    • std::chrono::time_point
    • std::chrono::duration
    • boost::optional<T>
    • std::pair<T1, T2>
    • std::tuple<Ts...>
    • std::basic_string<T> 等等。
常用生成器函数

RapidCheck 提供了一系列生成器,用于生成不同类型的数据。以下是一些常见的生成器:

  • gen::positive:生成正整数
  • gen::container:生成容器(如 std::vector
  • gen::suchThat:根据给定条件筛选生成的数据
  • gen::map:将一个生成器生成的数据应用一个函数
  • gen::unique:生成唯一的元素
  • gen::oneOf:从多个选项中随机选择一个
  • gen::inRange:生成指定范围内的数值
  • gen::character:生成字符
  • gen::string:生成字符串
示例:
  1. 生成正整数:
    using namespace rc;
    const auto myGen = gen::positive<int>();
    
  2. 生成正整数的 std::vector
    const auto myGen = gen::container<std::vector<int>>(gen::positive<int>());
    
  3. 仅生成偶数长度的 std::vector<int>
    const auto myGen = gen::suchThat(gen::container<std::vector<int>>(gen::positive<int>()),[](const auto &v) { return (v.size() % 2) == 0; }
    );
    
  4. 将生成的整数数组连接为字符串:
    const auto myStringGen = gen::map(myGen, [](const auto &v) { return joinElements(v, ", "); }
    );
    
状态测试(Stateful Testing)
  • 有时候,代码并不是纯函数,而是有状态的,这时候输入数据成为一系列的操作。
  • 状态测试的关键是通过模拟操作序列并验证与模型的符合程度。通过这种方式,可以测试具有副作用的函数。
测试实例:

以下是一些用 RapidCheck 进行的测试,展示了如何使用它来验证复杂的逻辑,像是 Spotify 的播放器系统:

  • 失败的测试用例
    在一系列操作后,发现了预期值与实际值的不匹配,最终通过 RC_ASSERT 输出了具体的错误:
    RC_ASSERT(track == expected_track);
    
    这一错误显示,播放器在播放两个不同的音轨时,返回的音轨 ID 应该是不同的,但实际上它们相同。
在Spotify的学习与收获
  • 高覆盖率,少量代码:通过使用属性测试,能够用少量的代码实现高覆盖率,快速发现潜在问题。
  • 处理复杂情况:RapidCheck 使得测试非常复杂的功能变得可行。尤其是那些需要大量模拟和状态操作的场景,属性测试在这些领域非常有价值。
  • 促使深入思考:它促使开发者更深入地思考程序应该如何工作,而不仅仅是它当前的行为。这有助于发现和修复潜在的设计问题。
  • 意外的 bug:在已有的代码中,使用属性测试发现了很多令人惊讶的 bug,这些 bug 可能在传统单元测试中没有被注意到。

总结:

RapidCheck 是一个强大的 C++ 库,可以帮助开发者进行属性测试,自动生成各种类型的数据,进行高效的错误检查。通过状态测试、属性测试和高覆盖率的方式,能够大大提高代码的质量和健壮性。它不仅可以处理简单的功能,还能处理复杂的操作序列,帮助开发者发现意想不到的 bug。

http://www.xdnf.cn/news/962029.html

相关文章:

  • 计算机基础(一):ASCll、GB2312、GBK、Unicode、UTF-32、UTF-16、UTF-8深度解析
  • 记录chrome浏览器的一个bug
  • 零基础入门 线性代数
  • 上位机开发过程中的设计模式体会(2):观察者模式和Qt信号槽机制
  • 经典的多位gpio初始化操作
  • 基于FPGA的PID算法学习———实现PI比例控制算法
  • React Native 基础语法与核心组件:深入指南
  • 篇章三 论坛系统——环境搭建
  • 如何将数据从 iPhone 传输到笔记本电脑
  • ACM70V-701-2PL-TL00
  • CPP基础(2)
  • Linux 删除登录痕迹
  • rapidocr v3.1.0发布
  • 什么样的登录方式才是最安全的?
  • 高频交易技术:订单簿分析与低延迟架构——从Level 2数据挖掘到FPGA硬件加速的全链路解决方案
  • Numpy7——数学2(矩阵基础,线性方程基础)
  • 看板会议如何高效进行
  • 设计模式和设计原则回顾
  • React动态渲染:如何用map循环渲染一个列表(List)
  • VsCode 离线插件下载
  • 第十三章 RTC 实时时钟
  • 从离散控制到集成管理:Modbus TCP转CANopen网关重构烟丝膨胀生产线
  • 如何使用 IP 地址修改 Android 的 Captive Portal 校验 URL
  • 关于Android camera2预览变形的坑
  • 《高等数学》(同济大学·第7版)第四章第二节换元积分法
  • 在GIS 工作流中实现数据处理
  • 天机学堂手撸
  • CentOS下的分布式内存计算Spark环境部署
  • 什么是MongoDB
  • freeCAD 学习 step1