文章目录
- 摘要
- 前言
- 什么是mock
- mock示例
- cmocka安装
- 使用mock function替换subfunction
- 控制mock function的输入和输出
摘要
本文介绍cmocka中,mock的使用。
前言
在这之前,需要了解最基本的cmocka使用。如果之前有gtest的编程经验,掌握cmocka的简单使用非常容易。这里有份入门文档,我不再赘述:使用 cmocka 进行单元测试 | 前尘逐梦
单元测试框架的基本逻辑是:
- 测试内容相似的,放在一个组。这个通常称为Test Fixtures(Using the Same Data Configuration for Multiple Tests)。
- 相同数据的准备和清除动作,通常由setup/teardown这两组函数来执行。
- 一个具体的单元测试中,通常使用断言(Assertions),来决定单元测试是否通过。
- 然后就是mock,这个有点高级,也是本文介绍的内容。
什么是mock
cmocka 是一个优雅的 C 单元测试框架,支持mock objects。它只需要标准的 C 库,可以在一系列计算平台(包括嵌入式)和不同的编译器上工作。
什么是mock呢?我问了下chatgpt, 它告诉我这些内容:
在单元测试中,Mock是一种测试技术,用于模拟(或称为替代)系统的组件或依赖项。当编写单元测试时,我们通常希望专注于测试当前正在编写的代码单元,而不是依赖于其他组件或外部系统。为了实现这一目标,我们可以使用Mock对象来替代这些依赖项,以便我们能够控制测试环境并验证代码的行为。使用Mock的主要目的是:
- 模拟外部依赖项:例如,当测试一个函数,它依赖于数据库或网络服务时,我们可以使用Mock对象代替实际的数据库或网络服务,以确保测试的独立性和可控性。
- 简化测试环境:有时,某些依赖项可能很难配置或设置,使用Mock对象可以避免设置这些复杂的依赖项,从而简化测试环境的搭建。
- 控制测试场景:Mock对象可以模拟各种情况和条件,使测试更全面和灵活。
嗯,说的挺好。但如果是第一次接触mock,还是会不明白。因为上面只是告诉了我们mock的好处/必要性,但是该怎么实现呢?
我们通过图片来说明,图片来自:Unit testing and mocking wi nd mocking with cmocka
当我们想对一个函数(function)进行单元测试时,但是这个函数内部调用了子函数(subfunction)。函数的结果由子函数决定,但是子函数的返回值是变化的/依赖外部数据库等。此时,想给函数添加单元测试,殊为不易。单元测试-函数-子函数的调用链如下所示:
此时,如果我们能在调用函数时,使用我们自定义的函数(mock function)替换掉子函数。并且,如果我们可以控制这个mock function的输入和输出,并将mock funciton的输出喂给函数,那么我们就可以给函数添加单元测试了。mock过程如下图所示:
mock示例
书接上文。mock的核心有两点:(1)单元测试中,使用mock function替换subfunction。(2)单元测试中,可以控制mock function的输入和输出。
talk is cheap, show me your code.
本文的示例代码来自:example/mock/uptime · master · cmocka / cmocka · GitLab
cmocka安装
首先我们需要先下载和编译上面的示例代码。
# 为了查看示例更加方便,需要下载源码编译
## 具体如何编译,查看里面的README.md和INSTALL.md
wget https://cmocka.org/files/1.1/cmocka-1.1.7.tar.xz
tar -xvf cmocka-1.1.7.tar.xz
cd cmocka-1.1.7.tar.xz
mkdir build && cd build
cmake .. -DBUILD_SHARED_LIBS=OFF
make
# 包管理器方式安装
## alamlinux8上,这个包中只包含动态库。如果需要静态链接,只好上面源码的方式集成
dnf install libcmocka-devel
使用mock function替换subfunction
这一步需要在链接(ld)的时候做。编译链接的选项中添加-Wl,--wrap=uptime
。可以看到示例的CMakeLists.txt中有这样的内容:
set_property(TARGET
test_uptime
PROPERTY
LINK_FLAGS
"${DEFAULT_LINK_FLAGS} -Wl,--wrap=uptime")
-Wl
告诉 GCC 将后面的选项传递给链接器 ld。关于--wrap
选项,我在gcc手册中没有找到。这个选项在man ld中。
Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to “__wrap_symbol”. Any undefined reference to “__real_symbol” will be resolved to symbol.
Only undefined references are replaced by the linker. So, translation unit internal references to symbol are not resolved to “__wrap_symbol”.
啥意思呢?要看明白上面的意思,还得先明白啥叫translation unit
。可参考:c++ faq - What is a “translation unit” in C++? - Stack Overflow
translation unit
定义:是C++中编译的基本单元。它由单个源文件的内容组成,加上它直接或间接包含的任何头文件的内容,减去使用条件预处理语句忽略的那些行。
以上一节的图为例,它的意思是,当function和subfunction位于不同文件时,使用--warp
选项后,function实际会去链接__wrap_subfunction
。
注意,function和subfunction不能在同一个translation unit
,否则--warp
选项无效(我踩过这个坑了)。相关内容可见:c - GCC’s linker --wrap will not wrap over static library function - Stack Overflow、 wrapper - How to wrap existing function in C - Stack Overflow
本节具体的代码示例,可见上面的链接。(这里有篇博客,也有示例代码,但是它的代码结构不好: GCC中通过–wrap选项使用包装函数-CSDN博客)
控制mock function的输入和输出
为了说明方便,我拷贝下示例代码,完整的代码见链接example/mock/uptime · master · cmocka / cmocka · GitLab。
由于链接选项中,设置了-Wl,--wrap=uptime
。所以当calc_uptime()
中调用uptime()
时,将会调用__wrap_uptime()
。链接过程帮我们做了函数替换。此时需要使用CMocka API - Mock Objects控制__wrap_uptime()
的输入输出了。
will_return()
: Store a value to be returned by mock()
later。
test_calc_uptime_minutes
调用了两个will_return()
,给__wrap_uptime
放入了两个值。__wrap_uptime
通过两次mock_type
取出。(内部的原理不清楚,但是使用还是蛮简单的嘛。)
int __wrap_uptime(const char *uptime_path,
double *uptime_secs,
double *idle_secs)
{
double up;
double idle;
/* Verify the passed value of the argument is correct */
check_expected_ptr(uptime_path);
/* Assign the return values */
up = mock_type(double);
idle = mock_type(double);
if (uptime_secs != NULL) {
*uptime_secs = up;
}
if (idle_secs != NULL) {
*idle_secs = idle;
}
return (int)up;
}
static void test_calc_uptime_minutes(void **state)
{
char *uptime_str = NULL;
UNUSED(state);
/* Make sure the passed 'in' argument is correct */
expect_string(__wrap_uptime, uptime_path, "/proc/uptime");
/* We tell the uptime function what values it should return */
will_return(__wrap_uptime, 508.16);
will_return(__wrap_uptime, 72.23);
/* We call the function like we would do it normally */
uptime_str = calc_uptime();
/* Now lets check if the result is what we expect it to be */
assert_non_null(uptime_str);
assert_string_equal(uptime_str, "up 8 minutes");
free(uptime_str);
}
这里,我再顺道介绍下expect_string
和check_expected_ptr
。这两个函数在cmocka API - Checking Parameters中。
test_calc_uptime_minutes
“大吼一声”,我要在calc_uptime
调用__wrap_uptime
时,检查下uptime_path
这个变量是不是"/proc/uptime"。test_calc_uptime_minutes
在执行前设置了期望;__wrap_uptime
在执行时,验证期望。