C 如何捕获对exit()的调用(用于单元测试)
我正在为一个个人项目编写一个动态数组,并尝试对所有函数进行单元测试。我正试图为C 如何捕获对exit()的调用(用于单元测试),c,unit-testing,exit,C,Unit Testing,Exit,我正在为一个个人项目编写一个动态数组,并尝试对所有函数进行单元测试。我正试图为util\u dyn\u array\u check\u index()编写一个单元测试,但在这样做时遇到了问题,因为我做出了设计决策,如果索引超出范围,则调用exit(-1)。我想在单元测试中检查它是否在提供无效索引时调用exit()。但如果我给它一个无效的索引,它就会退出我的测试程序。是否可能以某种方式捕捉到对exit()的调用被抛出,或者在我的测试程序中重新定义exit(),以防止它结束测试 从中,我研究了ate
util\u dyn\u array\u check\u index()
编写一个单元测试,但在这样做时遇到了问题,因为我做出了设计决策,如果索引超出范围,则调用exit(-1)
。我想在单元测试中检查它是否在提供无效索引时调用exit()
。但如果我给它一个无效的索引,它就会退出我的测试程序。是否可能以某种方式捕捉到对exit()
的调用被抛出,或者在我的测试程序中重新定义exit()
,以防止它结束测试
从中,我研究了atexit()
,但这看起来并没有停止退出,只是在退出之前执行一个或多个用户定义的函数。这对我不起作用,因为在这之后我还有其他测试要运行。我最后的想法是,我可以将util\u dyn\u array\u check\u index()
作为一个宏而不是一个函数,并在我的测试程序中将exit()
重新定义为一个不同的函数,但如果可以避免的话,我宁愿不将其作为一个宏
这是我的密码:
这个结构的细节并不重要,只是为了完整性而提供
//basically a Vec<T>
typedef struct {
//a pointer to the data stored
void * data;
//the width of the elements to be stored in bytes
size_t stride;
//the number of elements stored
size_t len;
//the number of elements able to be stored without reallocating
size_t capacity;
} util_dyn_array;
下面是我希望测试的框架(为了清晰起见,省略了我用来让编写测试变得更好的一些宏魔术)
bool test\u dyn\u array\u check\u index(){
util_dyn_数组向量=util_dyn_数组新(sizeof(int),16);
对于(int i=0;i<16;i++){
util_dyn_array_push(&vector,(void*)&i);
}
对于(int i=0;i<16;i++){
//如果什么都没发生,那就是成功了
util_dyn_数组_check_索引(&vector,i);
}
//以某种方式检查它是否调用exit,而不让它使我的程序崩溃
{
util_dyn_数组_check_索引(&vector,16);
}
返回true;
}
显然,我可以更改我的代码以返回
bool
或写入errno
,但我更希望它退出,因为它通常是一个不可恢复的错误。在标准C中定义另一个exit()
(如果包含头,则作为函数和宏)。不过,在许多环境中,您可能都能侥幸逃脱
话虽如此,在这里做这样的事情毫无意义,因为我们谈论的是exit()
。该函数不希望在此之后继续执行,因此几乎迫使您将其替换为longjmp()
,或者使用文本替换将其转换为返回类型(假设为void
返回类型)。在这两种情况下,都意味着您需要假设函数没有使事物处于中断状态(例如持有某些资源)。这是一个很大的假设,但如果您的单元测试框架要绑定到这个特定的项目,那么这可能是一个合理的解决方法
我建议您在测试框架中添加对在自己的流程中运行测试的支持,而不是试图修改被测试函数的行为。除了能够测试这些东西之外,还有很多好处。例如,您可以免费并行运行测试,并隔离测试之间的许多副作用。如果是单元测试,您不需要实际捕获对exit()
的调用
在解决方案之前,我建议重新设计您的库。你有几个选择:
- 让
util\u dyn\u array
包含一个回调,如果遇到越界模式,将调用该回调,并将其默认为exit(1)
(而不是exit(-1)
,当从shell调用程序时,该回调无法正常工作)
- 拥有一个全局越界处理程序(默认为
exit(1)
),并允许程序在运行时通过调用类似set\u oob\u处理程序(new\u handler)
的东西来更改处理程序
- 做集成测试而不是单元测试。正如这里的许多人所建议的,如果库可以退出或崩溃,这将进入集成领域(与调用进程/OS)
我的解决方案:
main.c
:
#include <stdio.h>
void func(void);
int main(int argc, char **argv)
{
printf("starting\n");
func();
printf("ending\n");
}
void my_exit(int status)
{
printf("my_exit(%d)\n", status);
#ifdef UNIT_TEST
printf("captured exit(%d)\n", status); // you can even choose to call a global callback here, only in unit tests.
#else
exit(status);
#endif
}
void func(void) {
my_exit(1);
}
makefile
:
# these targets are MUTUALLY EXCLUSIVE!!
release:
cc -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
fortest:
cc -DUNIT_TEST=1 -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
(请注意,这是在Linux上测试的,我没有Mac可供测试,因此可能需要对makefile进行较小的修改)。退出函数是一个弱符号,因此您可以创建自己的函数副本,以捕捉调用它的情况。此外,您可以在测试代码中使用setjmp
和longjmp
来检测要退出的正确调用:
例如:
#include "file_to_test.c"
static int expected_code; // the expected value a tested function passes to exit
static int should_exit; // 1 if exit should have been called
static int done; // set to 1 to prevent stubbing behavior and actually exit
static jmp_buf jump_env;
static int rslt;
#define test_assert(x) (rslt = rslt && (x))
// stub function
void exit(int code)
{
if (!done)
{
test_assert(should_exit==1);
test_assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}
bool test_dyn_array_check_index() {
int jmp_rval;
done = 0;
rslt = 1;
util_dyn_array vector = util_dyn_array_new(sizeof(int), 16);
for(int i = 0; i < 16; i++) {
util_dyn_array_push(&vector, (void*)&i);
}
for(int i = 0; i < 16; i++) {
//if nothing happens, its successful
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, i);
}
test_assert(jmp_rval==0);
}
// should call exit(-1)
{
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, 16);
}
test_assert(jmp_rval==1);
}
done = 1
return rslt;
}
#包括“文件到测试.c”
应为静态int_代码;//被测试函数传递给exit的期望值
静态int应_退出;//1如果应该调用exit
静态int完成;//设置为1以防止存根行为并实际退出
静态jmp_buf jump_env;
静态int-rslt;
#定义测试断言(x)(rslt=rslt&&(x))
//存根函数
无效退出(内部代码)
{
如果(!完成)
{
test_assert(应该_exit==1);
测试\断言(预期\代码==代码);
longjmp(jump_env,1);
}
其他的
{
_出口(代码);
}
}
布尔测试\u动态\u数组\u检查\u索引(){
int jmprval;
完成=0;
rslt=1;
util_dyn_数组向量=util_dyn_数组新(sizeof(int),16);
对于(int i=0;i<16;i++){
util_dyn_array_push(&vector,(void*)&i);
}
对于(int i=0;i<16;i++){
//如果什么都没发生,那就是成功了
应该退出=0;
如果(!(jmp_rval=setjmp(jump_env)))
{
util_dyn_数组_check_索引(&vector,i);
}
test_assert(jmp_rval==0);
}
//应呼叫出口(-1)
{
应退出=1;
预期的_代码=2;
如果(!(jmp_rval=setjmp(jump_env)))
{
util_dyn_数组_check_索引(&vector,16);
}
test_assert(jmp_rval==1);
}
完成=1
# these targets are MUTUALLY EXCLUSIVE!!
release:
cc -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
fortest:
cc -DUNIT_TEST=1 -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
$ make release
cc -g -c -fpic something.c
[...]
$ LD_LIBRARY_PATH=. ./main
starting
my_exit(1)
$ make fortest
cc -DUNIT_TEST=1 -g -c -fpic something.c
[...]
$ LD_LIBRARY_PATH=. ./main
starting
my_exit(1)
captured exit(1)
ending
#include "file_to_test.c"
static int expected_code; // the expected value a tested function passes to exit
static int should_exit; // 1 if exit should have been called
static int done; // set to 1 to prevent stubbing behavior and actually exit
static jmp_buf jump_env;
static int rslt;
#define test_assert(x) (rslt = rslt && (x))
// stub function
void exit(int code)
{
if (!done)
{
test_assert(should_exit==1);
test_assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}
bool test_dyn_array_check_index() {
int jmp_rval;
done = 0;
rslt = 1;
util_dyn_array vector = util_dyn_array_new(sizeof(int), 16);
for(int i = 0; i < 16; i++) {
util_dyn_array_push(&vector, (void*)&i);
}
for(int i = 0; i < 16; i++) {
//if nothing happens, its successful
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, i);
}
test_assert(jmp_rval==0);
}
// should call exit(-1)
{
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, 16);
}
test_assert(jmp_rval==1);
}
done = 1
return rslt;
}
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/wait.h>
void util_dyn_array_check_index() {
exit(-1);
}
int main(void) {
pid_t pid = fork();
assert(pid >= 0);
if( pid == 0 ) {
util_dyn_array_check_index();
exit(0);
}
else {
int child_status;
wait(&child_status);
printf("util_dyn_array_check_index exited with %d\n", WEXITSTATUS(child_status));
}
}
inline void util_dyn_array_check_index(util_dyn_array * self, size_t index) {
assert(index < self->len);
}
Assertion failed: (index < self->len), function util_dyn_array_check_index, file test.c, line 9.
void util_dyn_array_check_index() {
assert(43 < 42);
}
int main(void) {
pid_t pid = fork();
assert(pid >= 0);
if( pid == 0 ) {
// Suppress the assert output
fclose(stderr);
util_dyn_array_check_index();
exit(0);
}
else {
int status;
wait(&status);
if( WTERMSIG(status) == SIGABRT ) {
puts("Pass");
}
else {
puts("Fail");
}
}
}
#ifdef DEBUG
#define exit_badindex(...) my_debug_function(__VA_ARGS__)
#else
#define exit_badindex(...) exit(__VA_ARGS__)
#ifndef NDEBUG
#warning NDEBUG undefined in non-DEBUG build: my_debug_function will not be called
#endif
#endif