编写测试
这里我们来看下如何编写selftest。还是从简单的例子开始,在目录kselftest_harness中的代码就是很好的开始。
这个例子使用了新版本的测试框架,引用的是kselftest_harness.h头文件。老版本的引用的是kselftest.h。
从kselftest_harness.h文件的文档中,我们可以看到使用方法。结构比较清晰:
TEST/TEST_F来定义测试用例,写一个就表示一个用例
TEST定义的是比较简单的,没有额外的数据
TEST_F定义的相对复杂,可以用FIXTURE定义一个数据结构,并且在测试正式运行前后,通过FIXTURE_SETUP/FIXTURE_TEARDOWN来准备和销毁测试数据。
用ASSERT_EQ等宏来判断测试是否成功
用SKIP来上报跳过测试,如系统配置不符合等情况
定义测试用例的两种方式
例子中我们可以看到有两种定义测试用例的方式,TEST()简洁一些,TEST_F()有更多的定制空间。
TEST()
#define TEST(test_name) __TEST_IMPL(test_name, -1)
#define __TEST_IMPL(test_name, _signal) \
static void test_name(struct __test_metadata *_metadata); \
static void wrapper_##test_name( \
struct __test_metadata *_metadata, \
struct __fixture_variant_metadata __attribute__((unused)) *variant) \
{ \
test_name(_metadata); \
} \
static struct __test_metadata _##test_name##_object = \
{ .name = #test_name, \
.fn = &wrapper_##test_name, \
.fixture = &_fixture_global, \
.termsig = _signal, \
.timeout = TEST_TIMEOUT_DEFAULT, }; \
static void __attribute__((constructor)) _register_##test_name(void) \
{ \
__register_test(&_##test_name##_object); \
} \
static void test_name( \
struct __test_metadata __attribute__((unused)) *_metadata)这个定义还是比较简洁的:
定义了一个__test_metadata结构的变量(_##test_name##_object), 这个变量中包含了定义的代码块,预期的信号量,超时设置
通过构造函数_register_##test_name,将变量注册到某个链表上
TEST_F()
相比前面一个定义,这个要长很多。为了看清楚,我先把中间一个wrapper函数的定义省略。
这么来看,主要工作和上面差不多:
定义了一个__test_metadata结构的变量
定义了一个构造函数,将变量注册到链表上
区别是这次的变量,是通过mmap(MAP_SHARED)分配的。PS: 其实我不太懂,全局变量不应该也是大家都能访问的吗?
接下来我们就看看,刚才省略的,被写入object->fn的定义wrapper_##fixture_name##_##test_name。
测试的启动
当我们用kselftest_harness.h时,最后只要写上TEST_HARNESS_MAIN就行了。打开一看,实际上是调用了test_harness_run()。
那就让我们来仔细研究一下这个函数test_harness_run()。
从结构上来说,其实很清晰。就做了两件事情:
数一数一共有多少测试用例
运行测试,并记录测试结果
那我们就一个个展开吧。
帮助信息
看了test_harness_run()才知道,原来测试用例还有帮助。赶紧运行一下,看看都是啥。
其中-l这个选项很有意思,再执行一下看看。
从这个输出中,我们可以看到kselftest的测试用例有三个层次:
fixture
variant
test
运行时我们还可以通过参数-t/-f等来设定需要运行哪些测试用例。
选中测试用例
这个三层循环很明显告诉我们测试用例的三层结构:fixture/variant/test。
所以test是最小的粒度,一共运行了多少测试用例,说的就是这个数字。
其中test_enabled()用来比较命令行上传入的名字和f/v/t中的名字是否匹配。
运行测试用例
运行的整理过程和上面差不多,也是按照f/v/t的顺序。对需要运行的测试,则使用__run_test()
__run_test()的运行过程也不难,他会fork一个子进程来调用t->fn(),把返回的状态写到t->exit_code中。由__test_passed()来判断测试用例是否通过测试。
这样的话,问题就又回到了t->fn。这个就是在TEST()/TEST_F()中设置的值。
对TEST()来说,这个函数就是用户定义的样子,不过是套了一个壳子。但对TEST_F()来说就有点不一样了。因为这个套的壳子有点大: wrapper_##fixture_name##_##test_name
我们摘出比较关键的部分看一下:
还是fork了一个子进程来完成测试。进入子进程后,先setup,然后运行测试,最后teardown。
有意思的是teardown_fn在父子进程中,各运行了一次,只是第一个参数不同,表示了是在父进程还是在子进程执行清理工作。
这个和如何定义teardown有关:
FIXTURE_TEARDOWN : 在子进程中清理
FIXTURE_TEARDOWN_PARENT : 在父进程中清理
增加variant
在__run_test()中我们已经看到for循环中第二层是遍历fixture->variant链表。在之前的例子中我们都没有看到过,这里我们看一下是如何使用的。
我们找到了有使用variant的例子代码。
在这里,首先用FIXTURE_VARIANT定义一个你想要的数据结构,然后通过FIXTURE_VARIANT_ADD将这个结构添加到fixture->variant链表上。
通过一系列的调用传递,最后把variant->data传给实际运行的函数。而这个variant->data就是用FIXTURE_VARIANT定义的结构。
其中传递了variant->data的三个函数分别是:
fixture_name##_setup: FIXTURE_SETUP()定义的代码块
fixture_name##_##test_name: TEST_F()定义的代码块
teardown_fn: 基本可以认为是FIXTURE_TEARDOWN[_PARENT]定义的代码块
Last updated
Was this helpful?