主页:https://github.com/mity/acutest
Acutest 是 C/C++ 单元测试工具,旨在尽可能简单,不妨碍开发人员并最大限度地减少任何外部依赖性。
为了实现这一点,完整的实现位于单个 C 头文件中,其核心仅依赖于少数标准 C 库函数。
主要特点:
acutest.h
。main()
)。--tap
选项)。--xml-output=FILE
)。C++具体特点:
std::exception
,则在错误消息中写出what()
。Unix/Posix 特定功能:
clock_gettime()
),用户可以使用--time=real
(与--time
相同)和--time=cpu
测量测试执行时间。Linux具体特点:
Windows 特定功能:
--time
测量测试执行时间。macOS 特定功能:
任何实现一个或多个单元测试(包括acutest.h
)的C/C++模块都可以构建为独立程序。出于本文档的目的,我们将生成的二进制文件称为“测试套件”。然后执行该套件来运行测试,按照其命令行选项的指定。
我们说任何单元测试成功当且仅当:
要使用 Acutest,只需在实现一个或多个单元测试的 C/C++ 源文件的开头包含头文件acutest.h
即可。请注意,标头提供了main()
函数的实现。
#include "acutest.h"
每个测试都应该作为具有以下原型的函数来实现:
void test_example ( void );
测试可以使用一些预处理器宏来验证测试条件。它们可以多次使用,如果其中任何一个条件失败,则特定测试被视为失败。
TEST_CHECK
是最常用的测试宏,它仅测试布尔条件,如果条件计算结果为 false(或零),则失败。
例如:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_CHECK ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_CHECK ( mem != NULL );
}
请注意,测试应该完全相互独立。每当调用测试套件时,用户都可以按任何顺序在套件中运行任意数量的测试。此外,默认情况下,在支持的平台上,每个单元测试都作为独立(子)进程执行。
最后,测试套件源文件必须使用宏TEST_LIST
列出单元测试。该列表指定每个测试的名称(必须是唯一的)和指向实现该测试的函数的指针。我推荐在命令行上易于使用的名称:特别是避免空格和其他可能需要在 shell 中转义的特殊字符;还要避免将破折号 ( -
) 作为名称的第一个字符,因为它可能会被解释为命令行选项而不是测试名称。
TEST_LIST = {
{ "example" , test_example },
...
{ NULL, NULL } /* zeroed record marking the end of the list */
};
请注意,测试列表必须以归零记录结束。
对于基本的测试套件,这或多或少是您需要了解的全部内容。然而,Acutest 提供了更多的宏,这些宏在某些特定情况下很有用。我们将在以下小节中介绍它们。
有一个宏TEST_ASSERT
与TEST_CHECK
非常相似,但如果失败,它会立即中止当前单元测试的执行。
例如:
void test_example ( void )
{
void * mem ;
int a , b ;
mem = malloc ( 10 );
TEST_ASSERT ( mem != NULL );
mem = realloc ( mem , 20 );
TEST_ASSERT ( mem != NULL );
}
失败时的中止可以通过调用abort()
(如果测试作为子进程执行)或通过longjmp()
(如果不是)来执行。
因此,只有当您了解如此残酷的中止测试所带来的成本时,才应使用它。根据您的单元测试的作用,它可能包括未刷新的文件描述符、内存泄漏、在没有调用其析构函数的情况下被破坏的 C++ 对象等等。
一般来说, TEST_CHECK
应该优于TEST_ASSERT
,除非您确切地知道自己在做什么以及为什么在某些特定情况下选择TEST_ASSERT
。
对于 C++,有一个额外的宏TEST_EXCEPTION
用于验证给定代码(通常只是函数或方法调用)是否抛出预期类型的异常。
如果函数没有抛出任何异常或者抛出任何不兼容的内容,则检查失败。
例如:
void test_example ( void )
{
TEST_EXCEPTION ( CallSomeFunction (), std:: exception );
}
如果条件检查失败,提供一些有关情况的附加信息通常很有用,这样问题就更容易调试。 Acutest 为此提供了宏TEST_MSG
和TEST_DUMP
。
前一个输出任何类似printf
的消息,另一个输出所提供内存块的十六进制转储。
例如:
void test_example ( void )
{
char produced [ 100 ];
char expected [] = "Hello World!" ;
SomeSprintfLikeFunction ( produced , "Hello %s!" , "world" );
TEST_CHECK ( strcmp ( produced , expected ) == 0 );
TEST_MSG ( "Expected: %s" , expected );
TEST_MSG ( "Produced: %s" , produced );
/* Or, if the function could provide some binary stuff, we might rather
* use TEST_DUMP instead in order to output a hexadecimal dump of the data.
*/
TEST_DUMP ( "Expected:" , expected , strlen ( expected ));
TEST_DUMP ( "Produced:" , produced , strlen ( produced ));
}
请注意,只有当最近检查的宏失败时,两个宏才会输出任何内容。换句话说,这两者是等价的:
if (! TEST_CHECK ( some_condition != 0 ))
TEST_MSG ( "some message" );
TEST_CHECK ( some_condition != 0 );
TEST_MSG ( "some message" );
(请注意, TEST_MSG
需要具有可变参数宏支持的编译器。)
有时,将测试函数设计为数据循环,提供测试向量及其各自的预期输出的集合是很有用的。例如,假设我们的单元测试应该验证某种哈希函数的实现,并且我们在哈希规范中得到了它的测试向量集合。
在这种情况下,获取与每个测试向量相关的名称并在输出日志中输出该名称非常有用,这样如果任何检查失败,就可以轻松识别有罪的测试向量。然而,循环体可能会执行数十个检查宏,因此添加这样的名称来自定义循环中的每个检查消息可能是不切实际的。
为了解决这个问题,Acutest 提供了宏TEST_CASE
。该宏指定一个字符串作为测试向量名称。使用时,Acutest 确保在输出日志中提供的名称位于后续条件检查的任何消息之前。
例如,假设我们正在测试SomeFunction()
,对于给定的某个大小的字节数组,它应该在新的malloc
缓冲区中返回另一个字节数组。然后我们可以做这样的事情:
struct TestVector {
const char * name ;
const uint8_t * input ;
size_t input_size ;
const uint8_t * expected_output ;
size_t expected_output_size ;
};
struct TestVector test_vectors [] = {
/* some data */
};
void test_example ( void )
{
int i ;
const uint8_t * output ;
size_t output_size ;
for ( i = 0 ; i < sizeof ( test_vectors ) / sizeof ( test_vectors [ 0 ]); i ++ ) {
struct TestVector * vec = & test_vectors [ i ];
/* Output the name of the tested test vector. */
TEST_CASE ( vec . name );
/* Now, we can check the function produces what it should for the
* current test vector. If any of the following checking macros
* produces any output (either because the check fails, or because
* high `--verbose` level is used), Acutest also outputs the currently
* tested vector's name. */
output = SomeFunction ( vec -> input , vec -> input_size , & output_size );
if ( TEST_CHECK ( output != NULL )) {
TEST_CHECK ( output_size == vec -> expected_output_size );
TEST_CHECK ( memcmp ( output , vec -> expected_output , output_size ) == 0 );
free ( output );
}
}
}
指定的名称适用于使用TEST_CASE
后执行的所有检查
TEST_CASE
指定另一个名称;或者TEST_CASE
并以NULL
作为参数显式重置名称。前面几节中提到的许多宏都有一个对应的宏,它允许输出自定义消息而不是一些默认消息。
所有这些都与上述宏具有相同的名称,只是添加了下划线后缀。有了后缀,他们就会期望类似printf
的字符串格式和相应的附加参数。
因此,例如,而不是简单的检查宏
TEST_CHECK (a == b);
TEST_ASSERT (x < y);
TEST_EXCEPTION (SomeFunction(), std::exception);
我们可以将它们各自的对应部分与自定义消息一起使用:
TEST_CHECK_ (a == b, " %d is equal to %d " , a, b);
TEST_ASSERT_ (x < y, " %d is lower than %d " , x, y);
TEST_EXCEPTION_ (SomeFunction(), std::exception, "SomeFunction() throws std::exception");
您应该对它们使用一些中性措辞,因为使用命令行选项--verbose
,即使相应的检查成功通过,消息也会被注销。
(如果您需要在检查失败的情况下输出一些诊断信息,请使用宏TEST_MSG
。这正是它的目的。)
同样,代替
TEST_CASE ( "name" );
我们可以使用更丰富的
TEST_CASE_ ( "iteration #%d" , 42 );
但请注意,只有当您的编译器支持可变参数预处理器宏时,才能使用所有这些。随着 C99 的出现,可变参数宏成为 C 语言的标准部分。
当我们完成测试后,我们可以简单地将其编译为一个简单的 C/C++ 程序。例如,假设cc
是您的 C 编译器:
$ cc test_example.c -o test_example
编译测试套件后,生成的测试二进制文件可用于运行测试。
如果所有执行的单元测试都通过,则测试套件的退出代码为 0;如果其中任何一个测试失败,则测试套件的退出代码为 1;如果发生内部错误,则测试套件的退出代码为任何其他数字。
默认情况下(没有任何命令行选项),它运行所有已实现的单元测试。它还可以仅运行命令行上指定的单元测试的子集:
$ ./test_example # Runs all tests in the suite
$ ./test_example test1 test2 # Runs only tests specified
$ ./test_example --exclude test3 # Runs all tests but those specified
请注意,单个命令行参数可以选择一整组测试单元,因为 Acutest 实现了多个级别的单元测试选择(使用至少匹配一个测试单元的第一个):
完全匹配:当参数与单元测试的整个名称完全匹配时,则仅选择给定的测试。
单词匹配:当参数与任何完整的测试名称不匹配,但与一个或多个测试名称中的整个单词匹配时,则选择所有此类测试。
以下字符被识别为单词分隔符:空格
、制表符t
、破折号-
、下划线_
、斜杠/
、点.
, 逗号,
, 冒号:
, 分号;
。
子字符串匹配:如果连单词匹配都无法选择任何测试,则选择名称中包含参数作为其子字符串的所有测试。
通过采用适当的测试命名策略,这允许用户使用单个命令行参数轻松运行(或如果使用--exclude
则跳过)整个相关测试系列。
例如,考虑测试套件test_example
它实现了测试foo-1
、 foo-2
、 foomatic
、 bar-1
和bar-10
:
$ ./test_example bar-1 # Runs only the test 'bar-1' (exact match)
$ ./test_example foo # Runs 'foo-1' and 'foo-2' (word match)
$ ./test_example oo # Runs 'foo-1', 'foo-2' and 'foomatic' (substring match)
$ ./test_example 1 # Runs 'foo-1' and 'bar-1' (word match)
您可以使用--list
或-l
来仅列出给定测试套件实现的所有单元测试:
$ ./test_example --list
要查看所有支持的命令行选项的说明,请使用选项--help
运行二进制文件:
$ ./test_example --help
问:这个项目不是叫“CUTest”吗?
答:是的。由于发现原来的名称过于重载,因此已对其进行重命名。
问:我需要分发文件README.md
和/或LICENSE.md
吗?
答:不需要。标头acutest.h
包含我们存储库的 URL、版权说明以及其中的 MIT 许可条款。只要您保持这些完整,如果您仅将标头添加到项目中,我们就完全没问题。毕竟,它的简单使用和一体化标头性质是我们的主要目标。
Acutest 受 MIT 许可证保护,请参阅文件LICENSE.md
或acutest.h
开头以获取其全文。
该项目位于github上:
您可以在那里找到最新版本的 Acutest,贡献增强功能或报告错误。