cmocka是一个C语言的单元测试框架,仅依赖标准库,可以在多种平台多种编译器上使用。

cmocka官网为https://cmocka.org/

下载和编译

cmocka的源码托管在GitLab上,编译系统使用CMake

1
2
3
4
git clone https://gitlab.com/cmocka/cmocka.git
cd cmocka
mkdir build && cd build
cmake .. && make

示例

cmocka使用示例位于源码目录下的example文件夹中,example中演示了assert_macroassert_moduleallocate_modulemock的使用。

测试用法

  1. simple_test

    simple_test.c演示了cmocka最简单的使用方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <stdarg.h>
    #include <stddef.h>
    #include <setjmp.h>
    #include <stdint.h>
    #include <cmocka.h> // include cmocka header

    // test case, do nothing
    static void null_test_success(void **state) {
    (void) state; /* unused */
    }

    int main(void) {
    const struct CMUnitTest tests[] = {
    cmocka_unit_test(null_test_success),
    };

    return cmocka_run_group_tests(tests, NULL, NULL);
    }
  2. allocate_module_test

    该例子演示了allocate检测功能的使用, 对应的源码为example/allocate_module.cexample/allocate_module_test.c,其中allocate_module.c是待测试模块。

    根据源码分析,为了使用memory check功能,需要修改待测模块的源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #ifdef UNIT_TESTING
    extern void* _test_malloc(const size_t size, const char* file, const int line);
    extern void* _test_calloc(const size_t number_of_elements, const size_t size,
    const char* file, const int line);
    extern void _test_free(void* const ptr, const char* file, const int line);

    #define malloc(size) _test_malloc(size, __FILE__, __LINE__)
    #define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
    #define free(ptr) _test_free(ptr, __FILE__, __LINE__)
    #endif // UNIT_TESTING

    将代码中使用的mallocfree等函数替换成cmocka框架中的封装,然后在test case中调用待测函数

    1
    2
    3
    4
    static void leak_memory_test(void **state) {
    (void) state; /* unused */
    leak_memory();
    }

    示例程序中演示了检测内存泄漏、缓冲区溢出和缓冲区下溢;内存问题的检测通过替换mallocfree函数来完成,使用场景比较有限。

  3. assert_macro_test

    该示例演示了断言宏的使用,用法非常简单,在test case中使用断言宏对待测试模块进行调用即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    static void get_status_code_string_test(void **state) {
    (void) state; /* unused */
    assert_string_equal(get_status_code_string(0), "Address not found");
    assert_string_equal(get_status_code_string(1), "Connection timed out");
    }

    static void string_to_status_code_test(void **state) {
    (void) state; /* unused */
    assert_int_equal(string_to_status_code("Address not found"), 0);
    assert_int_equal(string_to_status_code("Connection timed out"), 1);
    }

    这些断言宏仅判断测试结果是否与预期相同。

  4. assert_module_test

    在这个例子中演示了assert相关宏的更高级的用法:

    • mock_assert

      example/assert_module.c中,使用mock_assert宏覆盖了标准库中的assert宏,原因是标准库中的assert宏会引起进程的Aborted,造成无法继续执行其他test case,而mock_assert不会引起进程Aborted

    • expect_assert_failure

      根据语义可以判断这个宏的作用是期望断言失败,即使用该宏测试的函数中发生断言失败,则该宏测试通过,否则测试失败。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      // 待测试函数
      void increment_value(int * const value) {
      assert(value);
      (*value) ++;
      }

      void decrement_value(int * const value) {
      if (value) {
      (*value) --;
      }
      }

      // test case
      static void increment_value_assert(void **state) {
      (void) state;
      expect_assert_failure(increment_value(NULL));
      }

      static void decrement_value_fail(void **state) {
      (void) state;
      expect_assert_failure(decrement_value(NULL));
      }

      // 测试结果
      [ RUN ] increment_value_assert
      Expected assertion value occurred
      [ OK ] increment_value_assert
      [ RUN ] decrement_value_fail
      Expected assert in decrement_value(NULL)
      [ ERROR ] --- [ LINE ] --- /home/noah/cmocka/example/assert_module_test.c:46: error: Failure!
      [ FAILED ] decrement_value_fail

mock用法

mock功能的演示代码位于example/mock中,提供了两个示例,分别是chef_wrapuptime

mock功能的使用依赖于一个连接器参数:--wrap=symbol,如果在编译时使用,需要用-Wl,--wrap=symbol,使用这个参数后,当需要调用symbol函数时,实际上会去调用__wrap_symbol

  • chef_wrap

    在这个例子中,待测模块是位于waiter_test_wrap.c中的waiter_process函数,该函数中使用了chef_cook函数,但是根据chef.cchef_cook函数的表述,该函数并未实现,所以需要对这个函数进行mock__wrap_chef_cook便是该函数的mock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    int __wrap_chef_cook(const char *order, char **dish_out)
    {
    bool has_ingredients;
    bool knows_dish;
    char *dish;

    check_expected_ptr(order); // 测试输入是否为期望值

    knows_dish = mock_type(bool); // mock knows_dish
    if (knows_dish == false) {
    return -1;
    }

    has_ingredients = mock_type(bool); // mock has_ingredients
    if (has_ingredients == false) {
    return -2;
    }

    dish = mock_ptr_type(char *); // mock dish
    *dish_out = strdup(dish);
    if (*dish_out == NULL) return ENOMEM;

    return mock_type(int); // mock return value
    }

    mock函数的实现中,有四处mock_type,这些变量的值由外部(如test case中)提供;check_expected_ptr宏用来测试变量是否为期望的值,该期望值也由外部指定。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    static void test_order_hotdog(void **state)
    {
    int rv;
    char *dish;
    (void) state; /* unused */

    /* 指定check_expected_ptr的期望值 */
    expect_string(__wrap_chef_cook, order, "hotdog");

    will_return(__wrap_chef_cook, true); // mock knows_dish
    will_return(__wrap_chef_cook, true); // mock has_ingredients
    /* mock dish */
    will_return(__wrap_chef_cook, cast_ptr_to_largest_integral_type("hotdog"));
    will_return(__wrap_chef_cook, 0); // mock return value

    rv = waiter_process("hotdog", &dish);

    assert_int_equal(rv, 0);
    assert_string_equal(dish, "hotdog");
    if (dish != NULL) {
    free(dish);
    }
    }
  • uptime

    该示例中编译后生成两个可执行文件,分别是uptimetest_uptime;其中uptime使用未被mockuptime函数,而test_uptime使用mockuptime函数。

    这个例子旨在演示mock函数的用途,以及开发过程中mock在进行单元测试时的作用。

生成测试报告

cmocka生成的xml格式报告为JUnit格式。

一般情况下,执行cmocka单元测试程序,测试结果会直接打印到stderr上,格式如下

1
2
3
4
5
6
7
[==========] Running 2 test(s).
[ RUN ] test_order_hotdog
[ OK ] test_order_hotdog
[ RUN ] test_bad_dish
[ OK ] test_bad_dish
[==========] 2 test(s) run.
[ PASSED ] 2 test(s).

如果需要生成xml格式报告,需要在代码中添加如下行

1
cmocka_set_message_output(CM_OUTPUT_XML);

该行需要在cmocka_run_group_tests之前调用;xml格式输出如下

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites>
<testsuite name="tests" time="0.000" tests="2" failures="0" errors="0" skipped="0" >
<testcase name="test_order_hotdog" time="0.000" >
</testcase>
<testcase name="test_bad_dish" time="0.000" >
</testcase>
</testsuite>
</testsuites>

除此之外,也可以通过设置CMOCKA_MESSAGE_OUTPUT环境变量修改cmocka的输出格式,环境变量可用的值有stdoutsubunittabxml;需要注意的是,设置环境变量修改报告格式的方法优先级更高。

默认情况下,cmocka的输出会被打印到stderr,如果需要存储到文件中,可以通过设置CMOCKA_XML_FILE环境变量的方式,如

1
CMOCKA_XML_FILE=testresults/result1.xml

如果cmocka无法在CMOCKA_XML_FILE指定的位置创建文件,则仍然会将结果输出到stderr

如果有多个cmocka测试程序需要生成报告,可以使用%g对文件名进行格式化

1
CMOCKA_XML_FILE=testresults/%g.xml

生成报告时,%g将被格式化为group name,即cmocka_run_group_tests宏的第一个参数,例如前面simple_test生成的报告文件名为tests.xml

生成覆盖率报告

推荐使用lcov工具生成代码覆盖率报告;lcov依赖于gcov,后者是包含在GNU编译套件中的,只要安装了GCC,一般就已经包含了gcov工具,但是lcov需要单独安装;Ubuntu上安装方法如下

1
sudo apt install lcov

为了生成代码覆盖率报告,首先需要在编译生成单元测试程序时,添加编译器和连接器参数

1
2
3
4
5
6
7
8
# 编译器参数
--coverage

# 连接器参数
--coverage -lgcov

# 通过GCC添加连接器参数
-Wl,--coverage -lgcov

编译完成后,可执行文件同级目录下应该会生成后缀名为.gcno的文件,如果使用cmake编译系统进行编译,生成的文件可能在对应项目的CMakeFiles目录中;继续执行测试程序,执行完成后,会在当前目录生成后缀名为.gcda的文件,如果使用cmake,则会生成在CMakeFiles对应的目录中。

接着使用lcov分析并生成对应的info文件

1
lcov --capture --directory project-dir --output-file coverage.info

注意将project-dir替换成包含.gcda文件的文件夹(支持递归查找)

最后使用genhtml工具将前面生成的info文件转为html格式的网页

1
genhtml coverage.info --output-directory out

生成的静态网页会存放在out文件夹中,使用浏览器打开index.html即可可视化查看代码覆盖率

代码覆盖率

覆盖的行