8.1相关准备
cmake -B <build_tree> -S <source_tree>
cmake --build <build_tree>
8.2 进行测试的必要性
8.2. 为什么自动化测试值得这么麻烦? 试想有一台机器在钢板上打孔的生产线。这些孔必须有特定的大小和形状,以便容纳螺栓,将 成品连接在一起。这种工厂线的设计者将设置机器,测试孔是否正确,然后继续工作。在未来,工 厂将使用不同的、更厚的钢材; 工人会不小心改变孔的尺寸; 或者,简单地说,需要打更多的孔,机 器必须升级。聪明的设计师会在生产线上的某些点进行质量控制检查,以确保产品符合规格并保留 其关键质量。孔必须符合特定的要求,不管是如何进行的打孔: 钻孔、打孔或激光切割。 同样的方法在软件开发中也有应用: 很难预测哪些代码段会保持多年不变,哪些代码段会多次 修改。随着软件功能的扩展,需要确保不会弄坏,即使是最好的程序员也会犯错误,他们不能预见 所做的更改。似乎这还不够,开发人员经常在别人写的代码上工作,不知道早期做出的复杂假设。 他们会阅读代码,建立大致的心理模型,添加必要的更改,并希望自己得到的正确结果。大多数情 况下,这是对的——可能事实并非如此,引入的 bug 可能需要花费数小时,甚至数天来修复,更不 用说它可能对产品和客户造成的损害了。 有时,会偶然发现一些非常难以理解和遵循的代码。不仅会质疑代码如何产生,以及做了什么, 而且可能还会开展一场政治迫害,以找出是谁造成了这样的混乱。这种事在我身上发生过,也可能 会发生在你身上。有时候,代码是匆忙创建的,并没有完全理解要解决的问题。 作为开发者,不仅面临着截止日期或预算的压力。当要求半夜醒来去修复一个严重的错误时, 肯定会惊讶于某些错误是如何通过代码检查的。 这些大部分都可以通过自动化测试来避免,这些代码块检查另一段代码 (在生产中使用) 的行为 是否正确。每次有人进行更改,自动化测试应该在没有提示的情况下执行。 这通常作为构建过程的一部分进行,并且作为在将代码合并到存储库之前控制代码质量的步 骤。 为了节省时间,可能会倾向于避免自动化测试,但这将会是一个代价高昂的教训。Steven Wright 说得很对:“经验是在需要它的时候才会得到的东西。”相信我: 除非是为个人目的编写一次性脚本 或为非生产实验创建原型,否则不要跳过编写测试。最初,可能会对精心编写的代码不断在测试中 失败的事实感到恼火。但认真考虑一下,失败的测试只是阻止向生产中添加破坏性修改。现在投入 的努力将会得到回报,因为修复 bug 的时间节省了 (以及一整晚的睡眠)。添加和维护测试也并不像 看起来那样困难。
全文背诵 这样就不会有煞笔同事喷你了。
8.3 使用CTest来标准化CMake中的测试
如何在已配置的项目上使用CTest执行测试?有三种操作模式: 1、测试 2、构建和测试 3、仪表盘客户端CDash(不讨论)
测试模式的命令行:
ctest [<options>]
这种模式下,使用cmake构建项目之后,应该在构建树中执行CTest。但这在开发模式中有点麻烦,因为需要多个命令来回更改工作目录。为了简化这个过程,CTest添加了第二个模式:构建-测试模式。
8.3.1 构建和测试模式
要使用这种模式,需要ctest有以--build-and-test的参数:
ctest --build-and-test <path-to-source> <path-to-build>
--build-generator <generator> [<options>...]
可以传递给ctest的选项在传递给ctest --build-and-test时都可以工作。
构建测试模式不会运行任何测试,除非在--testcommand后面提供ctest关键字。
一个例子:
ctest --build-and-test project/source-tree /tmp/build-tree --build-generator "Unix Makefiles" --test-command ctest
可以向此模式传递附件参数,分为三组,分别控制配置、构建过程和测试。 以下是控制配置阶段的参数:
· --build-options cmake配置的选项都应该在--test-command之前提供,--test-command位于最后。 · --build-two-config 为cmake运行两次配置阶段 · --build-nocmake 跳过配置阶段 · --build-generator-platform, --build-generator-toolset 提供生成器特定的平台和工具及。 · --build-makeprogram 当使用基于make和ninja的生成器时,需要指定make可执行文件。
以下是控制构建阶段的点:
· --build-target 构建指定的目标
· --build-noclean
· --build-project 提供所构建项目的名称
以下是用来控制测试阶段的参数
· --test-timeout 限制测试的执行(s为单位) · ... 常规测试模式下的参数
8.3.2 测试模式
查询测试
ctest -N
CTest提供了用LABELS关键字对测试进行分组的机制,列出所有可用的标签使用--print-labels。 尤其在使用add_test指令手动定义测试时,这个选项很有用。可以这样设置测试的标签:
set_target_propertiest(<name> PROPERTIES LABELS "<label>")
过滤测试
这些标志将根据提供的<r>
正则表达式(正则表达式)筛选测试:
· -R <r>, --test-regex <r> 只运行名称匹配<r>的测试
· -E <r>, --execlude-regex <r> 跳过名称匹配<r>的测试
· -L <r>, --label-regex <r> 只运行标签匹配<r>的测试
· -LE <r>, --label-exclude <regex> 跳过标签匹配<r>的测试
高级场景可以通过--test-information选项(-I)实现。使用此过滤器以逗号分隔的格式提供一个范围:start, end, step
。三个字段都可以为空。也可以单独指定单个测试的ID。
例子:
-I 3,, 从第三个测试开始执行
-I ,2, 从第一个测试开始到第二个测试
-I 2,,3 从一行中的第二个测试开始,每个第三个测试运行一次
-I ,0,,3,9,7 只测试3、9、7
默认情况下与-R一起使用的-I选项将缩小执行范围,同时满足两个需求的测试才会运行。如果要运行并集,使用-U选项即可。
乱序测试
--force-new-ctest-process 强制使用单独的进程 --schedulerandom
处理失败
测试用例输出错误的确切位置可以使用: --output-on-failure参数 或者设置CTEST_OUTPUT_ON_FAILURE也可以。
测试失败后停止执行: --stop-on-failure参数
CTest会存储运行失败的测试名称,可以跳过成功的测试,通过--return-failed参数直接从失败的测试开始。
将缺乏测试解释为错误,通过参数--no-test=error即可。
重复测试
通过-repeat <model>:<#>
反复运行测试。
model有三种可选模式:
· until-fail 运行测试<#>次;所有的测试都必须通过 · until-pass 运行测试<#>次;至少要通过一次。这在处理已知的不稳定、但太难调试或不能禁用的测试时非常有用。 · after-timeout 运行测试直到<#>次,但只有在测试超时时才充实。可以在负载过高的测试环境中使用。
控制输出
--output-failure --verbose --extar-verbose --quiet --output-log --test-output-size-passed --test-output-size-failed 其他选项
-C <cfg>, --build-config <cfg> 指定要测试的配置
-j <jobs>, --parallel <jobs> 并行测试
--test-load <level> 是CPU负载不超过<level>值
--timeout <seconds> 指定单个测试的默认时间限制
8.4 为CTest创建最基本的单元测试
// src/main.cpp
#include <iostream>
#inlcude "calc/calc.h"
int main(){
Calc c;
std::cout << "2+2=" << c.Sum(2,2) << std::endl;
std::cout << "3*3=" << c.Multiply(3,3) << std::endl;
}
// include/calc/calc.h
#pragma once
class Clac{
public:
int Sum(int, int);
int Multiply(int, int);
}
// src/calc.cpp
#include "calc/calc.h"
int Calc::Sum(int a, int b){
return a + b;
}
int Calc::Multiply(int a, int b){
return a * b;
}
// test/calc_test.cpp
#include "calc/calc.h"
#include <cstdlib>
void SumAddsTwoIntegers(){
Calc sut;
if(4 != sut.Sum(2,2)) std::exit(1);
}
void MultiplyMultipleTwoIntegers() {
Calc sut;
if(3 != sut.Multiply(1,3)) std::exit(1);
}
// unit_tests.cpp
#include <string>
void SumAddsTwoIntegers();
void MultiplyMultipliesTwoIntegers();
int main(int argc, char *argv[]){
if(argc < 2 || argv[1] == std::string("1"))
SumAddsTwoIntegers();
if(argc < 2 || argv[1] == std::string("2"))
MutiplyMutipliesTwoIntergers();
}
// CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
project(NoFrameworkTests CXX)
enable_testing()
add_subdirectories(src bin)
add_subdirectories(test)
// src/CMakeLists.txt
add_executable(main main.cpp calc.cpp)
target_include_directories(main PRIVATE ../include)
// test/CMakeLists.txt
add_executable(unit_test unit_tests.cpp calc_test.cpp ../src/calc.cpp)
target_include_directories(unit_test PRIVATE ../src)
add_test(NAME SumAddTwoInts COMMAND unit_tests 1)
add_test(NAME MutilyMutipliesTwoInts COMMAND unit_tests 2)