前言
最近一段时间在写C语言的课程设计,之前在用 Golang 的时候,Golang 自带单元测试,用起来非常舒服,而C语言不使用框架写测试则会很麻烦,下面通过一个简单的项目来实践在C语言中进行单元测试。
项目中使用 CMocka 作为单元测试框架,使用 CodeCov 检查代码覆盖率。
完整项目代码可以在 GitHub 上查看:c-unittest-example
项目目录
.
├── CMakeLists.txt
├── Makefile
├── README.md
├── cmake
│ ├── CMocka.cmake
│ └── CodeCov.cmake
├── include
│ └── add.h
├── src
│ └── add.c
└── test
├── CMakeLists.txt
├── add_tests.c
└── test.h
4 directories, 10 files
目录说明
cmake
: 存放 CMake 的模块文件,包括 CMocka 和 CodeCov。
include
: 项目头文件
src
: 项目源代码
test
: 单元测试代码
项目设置文件
Makefile
用于便携执行单元测试和构建程序
.PHONY: cmake test
BUILD_TYPE ?= Debug
BUILD_DIR ?= cmake-build-$(shell echo $(BUILD_TYPE) | tr '[:upper:]' '[:lower:]')
CODECOV ?= OFF
IWYU ?= ON
TEST_SUITES = add_tests
# 清理文件
clean:
@rm -rf $(BUILD_DIR)
# 创建 cmake-build-debug,并在里面执行 cmake
cmake:
@mkdir -p $(BUILD_DIR) && cd $(BUILD_DIR) && cmake -DCODE_COVERAGE=$(CODECOV) -DIWYU=$(IWYU) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) -j 4 ..
# 构建文件
build: cmake
@cd $(BUILD_DIR) && make project -j 4
# 进行单元测试
test:
@cd $(BUILD_DIR) && make $(TEST_SUITES) test CTEST_OUTPUT_ON_FAILURE=TRUE
# 测试代码覆盖率
coverage: test
@cd $(BUILD_DIR) && make codecov CMAKE_BUILD_TYPE=$(BUILD_TYPE)
cmake/CMocka.cmake
添加 CMocka 到项目中
include(ExternalProject)
# 添加额外的项目,即 CMocka
ExternalProject_Add(cmocka_ep
URL https://git.cryptomilk.org/projects/cmocka.git/snapshot/cmocka-1.1.5.tar.gz
CMAKE_ARGS -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME}
-DBUILD_STATIC_LIB=ON
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG:PATH=Debug
-DCMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE:PATH=Release
-DUNIT_TESTING=OFF
BUILD_COMMAND $(MAKE) cmocka-static
INSTALL_COMMAND "")
# 全局作用域下添加 cmocka 静态库
add_library(cmocka STATIC IMPORTED GLOBAL)
# 获取二进制文件夹路径
ExternalProject_Get_Property(cmocka_ep binary_dir)
# 分别设置正常、Debug、Release的导入路径
set_property(TARGET cmocka PROPERTY IMPORTED_LOCATION "${binary_dir}/src/libcmocka-static.a")
set_property(TARGET cmocka PROPERTY IMPORTED_LOCATION_DEBUG "${binary_dir}/src/Debug/libcmocka-static.a")
set_property(TARGET cmocka PROPERTY IMPORTED_LOCATION_RELEASE "${binary_dir}/src/Release/libcmocka-static.a")
# 将 cmocka_ep 依赖添加到 cmocka
add_dependencies(cmocka cmocka_ep)
# 获取 cmocka_ep 的源文件路径
ExternalProject_Get_Property(cmocka_ep source_dir)
# 全局作用域下设置头文件引入路径
set(CMOCKA_INCLUDE_DIR ${source_dir}/include GLOBAL)
cmake/CodeCov.cmake
添加 CodeCov 到项目中
# 寻找 gcovr
find_program(GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
# 设置 CMake 时的参数
set(CMAKE_C_FLAGS_CODECOV "-O0 -g --coverage" CACHE INTERNAL "")
# 设为高级变量
mark_as_advanced(CMAKE_C_FLAGS_CODECOV)
# 确保是在使用 Debug 来构建
string(TOLOWER ${CMAKE_BUILD_TYPE} current_build_type)
if (NOT current_build_type STREQUAL "debug")
message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
endif ()
# 如果使用的是 GNU 则链接 gcov 库文件
if (CMAKE_C_COMPILER_ID STREQUAL "GNU")
link_libraries(gcov)
endif ()
# 添加 CodeCov 编译参数到 CMake
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_CODECOV}")
message(STATUS "Appending code coverage compiler flags: ${CMAKE_C_FLAGS_CODECOV}")
# 添加自定义 target
add_custom_target(codecov
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
COMMENT "Generating code cov report at ${PROJECT_BINARY_DIR}/codecov.xml"
# 在 SHELL 中展示代码覆盖率总结
COMMAND ${GCOVR_PATH} --exclude-throw-branches -r .. --object-directory "${PROJECT_BINARY_DIR}" -e ".*/test/.*" -e ".*/usr/.*" --print-summary
# 输出到 codecov.xml
COMMAND ${GCOVR_PATH} --xml --exclude-throw-branches -r .. --object-directory "${PROJECT_BINARY_DIR}" -e ".*/test/.*" -e ".*/usr/.*" -o codecov.xml)
CMakeLists.txt
设置当前项目
# CMake 最低版本要求
cmake_minimum_required(VERSION 3.17)
# 设置 CMake 模块目录
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# 项目名
project(project)
# 设置 CodeCov
option(CODE_COVERAGE "Enable coverage reporting" OFF)
if (CODE_COVERAGE AND CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
include(CodeCov)
endif ()
# 设置 include-what-you-use
option(IWYU "Run include-what-you-use with the compiler" OFF)
if (IWYU)
# 寻找 iwyu
find_program(IWYU_COMMAND NAMES include-what-you-use iwyu)
if (NOT IWYU_COMMAND)
message(FATAL_ERROR "CMAKE_IWYU is ON but include-what-you-use is not found!")
endif ()
# 添加饮用
set(CMAKE_C_INCLUDE_WHAT_YOU_USE "${IWYU_COMMAND};-Xiwyu")
endif ()
# 启用测试
enable_testing()
# 将源码添加到项目
add_library(project
src/add.c)
# 设置头文件目录
target_include_directories(project
PUBLIC
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src)
# 添加单元测试子文件
add_subdirectory(test)
test/CMakeLists.txt
设置项目的单元测试
# 引入 CMocka.cmake
include(CMocka)
# 引入 Cmocka 头文件
include_directories(${CMOCKA_INCLUDE_DIR})
# 用于快捷添加测试,_testName 为单元测试的文件名,不带后缀
function(add_test_suite _testName)
add_executable(${_testName} ${_testName}.c)
target_link_libraries(${_testName} project cmocka)
add_test(${_testName} ${CMAKE_CURRENT_BINARY_DIR}/${_testName})
endfunction()
add_test_suite(add_tests)
项目源码
这里实现一个可以传可变参数的加法函数。
add.h
#ifndef PROJECT_ADD_H
#define PROJECT_ADD_H
int add(int count, ...);
#endif //PROJECT_TEST_H
add.c
#include "add.h"
#include <stdarg.h>
int add(int count, ...) {
va_list arg_ptr;
va_start(arg_ptr, count);
int sum = 0;
for (int i = 0; i < count; ++i) {
int tmp = va_arg(arg_ptr, int);
sum += tmp;
}
va_end(arg_ptr);
return sum;
}
test/test.h
引入 CMocka 所需的头文件
#ifndef PROJECT_TEST_H
#define PROJECT_TEST_H
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
int run_all_tests(void);
int main() {
return run_all_tests();
}
#endif //PROJECT_TEST_H
test/add_tests.c
#include "test.h"
#include "add.h"
static void test_add_1(void **state) {
int sum = add(1, 1);
assert_int_equal(1, sum);
}
static void test_add_2(void **state) {
int sum = add(2, 1, 2);
assert_int_equal(3, sum);
}
static void test_add_more(void **state) {
int sum = add(6, 1, 2, 3, 4, 5, 6);
assert_int_equal(21, sum);
}
int run_all_tests() {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_add_1),
cmocka_unit_test(test_add_2),
cmocka_unit_test(test_add_more),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
进行测试
项目根目录下执行
CODECOV=ON IWYU=OFF make cmake coverage
执行效果如下
到 CodeCov 上创建项目,获取 TOKEN,执行如下命令上传测试报告
echo "YOUR TOKEN" > .cc_token
bash <(curl -s https://codecov.io/bash) -f cmake-build-debug/codecov.xml -t @.cc_token
项目也可以使用 GitHub Actions 进行自动化构建和上传
在项目中设置 secrets.CODECOV_TOKEN
即可,详细设置可以查看 .github/workflows/workflow.yml