01 前言
在嵌入式软件开发中,单元测试是非常重要的一环。它可以帮助我们在开发过程中及时发现代码中的问题,提高代码的质量。目前,有很多单元测试框架可以使用,比如 Ceedling、Google Test 等。
本篇文章的主角 Ceedling 就是众多框架中的一个, Ceedling 是一个基于 Ruby 的 C 语言单元测试框架,它将CMock、Unity 和 CException 结合在一起,可以帮助我们快速搭建单元测试环境。本篇文章主要介绍如何使用 CMake、Ceedling 并结合 APM32 DAL 库在 vscode 中进行单元测试。
02 环境准备
下面是使用到的工具、软件和环境及其官方链接。
测试工程
在 github 获取测试工程 APM32 Ceedling Example。
工具
如果不想使用命令行,可以使用 vscode 和 Ceedling Test Explorer 插件进行单元测试,以下是一些用到的工具和插件。
依赖
下面是测试工程的依赖,具体的安装配置过程可以参考官方文档,这里就不赘述了。要注意的是,Ceedling 是基于 Ruby 环境的,需要先安装 Ruby 环境。
03 Ceedling 单元测试
Ceedling 是一个工具集合,包含了 CMock、Unity 和 CException 等工具。所以 CMock、Unity 和 CException 的功能也可以在 Ceedling 中使用。下面将一一介绍这些工具的使用方法。
Ceedling 项目配置
github 上的测试工程已经配置好了 Ceedling 项目,可以使用 Ceedling 命令或 CMake custom target 进行单元测试。如果需要自己配置 Ceedling 项目,可以使用 ceedling new
命令创建一个新的 Ceedling 项目,然后在 project.yml
文件中配置项目路径。
:paths:
:test:
- +:test/**
- -:test/support
:source:
- src/**
- startup/**
- ../driver/APM32F4xx_DAL_Driver/**
- ../driver/CMSIS/Include/**
- ../driver/Device/Geehy/APM32F4xx/**
- include/**
:support:
- test/support
:libraries: []
Unity 断言测试结果
Ceedling 使用 Unity 进行断言测试,所以单元测试中可以直接使用 Unity 的断言宏来测试函数的返回值、参数等。要使用 Unity 断言,需要在测试文件中包含 unity.h
头文件。
#include "unity.h"
#include "calculator.h"
void setUp(void)
{
}
void tearDown(void)
{
}
void test_addition(void)
{
TEST_ASSERT_EQUAL_UINT32(5, addition(2,3));
}
void test_assert(void)
{
TEST_ASSERT_EQUAL_INT32(1, 1);
TEST_ASSERT_EQUAL_INT64(1, 1);
TEST_ASSERT_EQUAL_UINT8(1, 1);
TEST_ASSERT_EQUAL_UINT16(1, 1);
TEST_ASSERT_EQUAL_UINT32(1, 1);
TEST_ASSERT_EQUAL_UINT64(1, 1);
TEST_ASSERT_EQUAL_PTR(&test_assert, &test_assert);
TEST_ASSERT_EQUAL_STRING("test_assert", "test_assert");
TEST_ASSERT_EQUAL_MEMORY("test_assert", "test_assert", 12);
TEST_ASSERT_NOT_EQUAL(0, 1);
TEST_ASSERT_NOT_EQUAL_INT(0, 1);
TEST_ASSERT_NOT_EQUAL_UINT(0, 1);
TEST_ASSERT_NOT_EQUAL_HEX8(0x00, 0x01);
TEST_ASSERT_NOT_EQUAL_HEX16(0x00, 0x01);
TEST_ASSERT_NOT_EQUAL_HEX32(0x00, 0x01);
TEST_ASSERT_NOT_EQUAL_HEX64(0x00, 0x01);
}
先切换到测试工程目录,然后使用 ceedling test:all
命令编译并运行测试文件,可以看到测试结果。
cd test
ceedling test:all
Test 'test_assert.c'
--------------------
Running test_assert.out...
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 1
PASSED: 1
FAILED: 0
IGNORED: 0
CMock 模拟对象
Ceedling 使用 CMock 进行对象的模拟,在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便进行测试。而这个虚拟的对象就是 mock 对象。简单来讲 mock 对象就是实际测试对象在调试期间的代替品。
由于单元测试的对象仅是当前单元,所以就要求所有的内部或者外部依赖项都应该是稳定的,采用 mock 的方法模拟跟本单元依赖的其他单元,可以将测试重点放在当前单元功能,排除其他单元的影响。
而 Ceedling 中使用的 CMock 会为在头文件中检测到的函数生成一个 mock 函数供测试使用。要使用 CMock,需要在 project.yml 文件中配置相关参数。参数的详细说明可以参考官方文档。
:cmock:
:mock_prefix: mock_ # Set the prefix for mock objects
:when_no_prototypes: :warn # Set to :warn to print a warning when a function is called without a prototype
:enforce_strict_ordering: TRUE # Set to TRUE to enforce strict ordering of expected calls
:plugins:
- :ignore # Allows ignoring functions from being mocked
- :callback # Allows setting a callback function for a mock
- :expect_any_args # Allows setting a mock to expect any arguments
- :return_thru_ptr # Allows setting a mock to return a value through a pointer
:treat_as:
uint8: HEX8
uint16: HEX16
uint32: UINT32
int8: INT8
bool: UINT8
上面配置中设置了 mock 函数的前缀为 mock_
,所以如果想测试 ‘apm32f4xx_dal_gpio.h’ 头文件中的函数,则需要在测试文件中包含 mock_apm32f4xx_dal_gpio.h
头文件。
#include "unity.h"
#include "mock_apm32f4xx_dal_gpio.h"
此时编译测试文件,会在 build/test/mocks
目录下生成 mock_apm32f4xx_dal_gpio.h
头文件,里面包含了所有在 apm32f4xx_dal_gpio.h
头文件中的函数的 mock 函数。可以看到 CMock 为每个函数生成了一系列的宏定义,比如 DAL_GPIO_WritePin_Ignore()
、DAL_GPIO_WritePin_Expect()
、DAL_GPIO_WritePin_ReturnThruPtr_GPIOx()
等。
这些宏和函数用于控制 DAL_GPIO_WritePin 函数在单元测试中的行为,包括忽略调用、设置期望参数、添加回调函数以及通过指针返回值。
#define DAL_GPIO_WritePin_Ignore() DAL_GPIO_WritePin_CMockIgnore()
void DAL_GPIO_WritePin_CMockIgnore(void);
#define DAL_GPIO_WritePin_StopIgnore() DAL_GPIO_WritePin_CMockStopIgnore()
void DAL_GPIO_WritePin_CMockStopIgnore(void);
#define DAL_GPIO_WritePin_ExpectAnyArgs() DAL_GPIO_WritePin_CMockExpectAnyArgs(__LINE__)
void DAL_GPIO_WritePin_CMockExpectAnyArgs(UNITY_LINE_TYPE cmock_line);
#define DAL_GPIO_WritePin_Expect(GPIOx, GPIO_Pin, PinState) DAL_GPIO_WritePin_CMockExpect(__LINE__, GPIOx, GPIO_Pin, PinState)
void DAL_GPIO_WritePin_CMockExpect(UNITY_LINE_TYPE cmock_line, GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
typedef void (* CMOCK_DAL_GPIO_WritePin_CALLBACK)(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState, int cmock_num_calls);
void DAL_GPIO_WritePin_AddCallback(CMOCK_DAL_GPIO_WritePin_CALLBACK Callback);
void DAL_GPIO_WritePin_Stub(CMOCK_DAL_GPIO_WritePin_CALLBACK Callback);
#define DAL_GPIO_WritePin_StubWithCallback DAL_GPIO_WritePin_Stub
#define DAL_GPIO_WritePin_ReturnThruPtr_GPIOx(GPIOx) DAL_GPIO_WritePin_CMockReturnMemThruPtr_GPIOx(__LINE__, GPIOx, sizeof(GPIO_TypeDef))
#define DAL_GPIO_WritePin_ReturnArrayThruPtr_GPIOx(GPIOx, cmock_len) DAL_GPIO_WritePin_CMockReturnMemThruPtr_GPIOx(__LINE__, GPIOx, cmock_len * sizeof(*GPIOx))
#define DAL_GPIO_WritePin_ReturnMemThruPtr_GPIOx(GPIOx, cmock_size) DAL_GPIO_WritePin_CMockReturnMemThruPtr_GPIOx(__LINE__, GPIOx, cmock_size)
void DAL_GPIO_WritePin_CMockReturnMemThruPtr_GPIOx(UNITY_LINE_TYPE cmock_line, GPIO_TypeDef* GPIOx, size_t cmock_size);
在测试文件中,可以使用这些宏定义来进行测试。比如测试 DAL_GPIO_WritePin 函数,可以使用 DAL_GPIO_WritePin_Expect()
宏定义来设置期望参数,然后调用 DAL_GPIO_WritePin()
函数。使用 DAL_GPIO_ReadPin_ExpectAndReturn()
宏定义来设置期望参数和返回值,然后调用 DAL_GPIO_ReadPin()
函数。
void test_DAL_GPIO_WritePin_SetLow(void)
{
DAL_GPIO_WritePin_Expect(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET);
DAL_GPIO_WritePin(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET);
DAL_GPIO_ReadPin_ExpectAndReturn(GPIOE, GPIO_PIN_5, GPIO_PIN_RESET);
uint8_t pinState = DAL_GPIO_ReadPin(GPIOE, GPIO_PIN_5);
TEST_ASSERT_EQUAL(GPIO_PIN_RESET, pinState);
}
使用 ceedling test:all
命令编译并运行测试文件,可以看到测试结果。
cd test
ceedling test:all
Test 'test_gpio.c'
------------------
Running test_gpio.out...
--------------------
OVERALL TEST SUMMARY
--------------------
TESTED: 2
PASSED: 2
FAILED: 0
IGNORED: 0
gcov 生成覆盖率报告
覆盖率是单元测试中一个很重要的指标,它可以帮助我们了解测试用例的覆盖情况,帮助我们发现测试用例的不足之处。Ceedling 也可以生成覆盖率报告,只需要在 project.yml
文件中配置相关参数即可。
:gcov:
:reports:
- HtmlDetailed
:gcovr:
:html_medium_threshold: 75
:html_high_threshold: 90
:plugins:
:load_paths:
- "#{Ceedling.load_path}"
:enabled:
- stdout_pretty_tests_report
- module_generator
- gcov # Add this to the list of enabled plugins to generate a coverage report
然后使用 ceedling gcov:all utils:gcov
命令生成覆盖率报告。
cd test
ceedling gcov:all utils:gcov
Test 'test_assert.c'
--------------------
Running test_assert.out...
Test 'test_calculator.c'
------------------------
Running test_calculator.out...
Test 'test_gpio.c'
------------------
Running test_gpio.out...
Creating gcov results report(s) in 'build/artifacts/gcov'... (WARNING) Deprecated option --branches used, please use '--txt-metric branch' instead.
(INFO) Reading coverage data...
(INFO) Writing coverage report...
Done in 0.484 seconds.
--------------------------
GCOV: OVERALL TEST SUMMARY
--------------------------
TESTED: 7
PASSED: 7
FAILED: 0
IGNORED: 0
---------------------------
GCOV: CODE COVERAGE SUMMARY
---------------------------
calculator.c Lines executed:90.00% of 10
calculator.c Branches executed:100.00% of 2
calculator.c Taken at least once:50.00% of 2
calculator.c No calls
也可以用 gcovr
转换成 HTML
格式的覆盖率报告。
cd test
ceedling gcov:all
gcovr -r . --html --html-details -o build/artifacts/gcov/index.html
运行完毕后可以在 build/artifacts/gcov
目录下找到。在浏览器中打开 index.html
文件,可以看到更直观的覆盖率报告,包括函数覆盖率、行覆盖率、分支覆盖率等。
04 在 vscode 中使用 Ceedling
如果想要优雅的进行单元测试,可以使用 vscode 和 Ceedling Test Explorer 插件。
安装插件
在 vscode 中安装 Test Explorer UI、Test Adapter Converter 和 Ceedling Test Adapter 插件。
配置项目
在项目根目录下创建 .vscode
文件夹,然后在 .vscode
文件夹下创建 settings.json
文件,配置项目路径。
{
"ceedlingExplorer.projectPath": "test"
}
配置 Ceedling 工程输出 xml 测试报告,供 Test Explorer 插件使用。
:junit_tests_report:
:artifact_filename: report_junit.xml # The name of the JUnit report file
:plugins:
:load_paths:
- "#{Ceedling.load_path}"
:enabled:
- stdout_pretty_tests_report
- module_generator
- gcov # Add this to the list of enabled plugins to generate a coverage report
- xml_tests_report # Add this to the list of enabled plugins to generate an XML report for Ceedling Test Explorer
- junit_tests_report # Add this to the list of enabled plugins to generate a JUnit report
运行测试
第一次配置完成后,需要刷新 Ceedling Test Explorer
插件,然后在 Test Explorer
窗口中点击 Reload
按钮,就可以看到测试用例了。
在具体测试用例上方,也可以选择 Run
或 Debug
按钮,进行测试用例的运行或调试。
本篇文章用到的 CMake
工程和 Ceedling
单元测试工程可以在 github 仓库下载。
本篇文章就到这里,希望对大家有所帮助。如果有能帮助到大家的地方,欢迎点个小星星。
参考资料