发新帖本帖赏金 80.00元(功能说明)我要提问
返回列表
打印
[开发工具]

基于 Ceedling 的嵌入式软件单元测试

[复制链接]
1147|4
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 susutata 于 2024-7-30 16:31 编辑

#申请原创# @21小跑堂

基于 Ceedling 的嵌入式软件单元测试




## 01 前言
在嵌入式软件开发中,单元测试是非常重要的一环。它可以帮助我们在开发过程中及时发现代码中的问题,提高代码的质量。目前,有很多单元测试框架可以使用,比如 Ceedling、Google Test 等。

本篇文章的主角 Ceedling 就是众多框架中的一个, Ceedling 是一个基于 Ruby 的 C 语言单元测试框架,它将CMock、Unity 和 CException 结合在一起,可以帮助我们快速搭建单元测试环境。本篇文章主要介绍如何使用 CMake、Ceedling 并结合 APM32 DAL 库在 vscode 中进行单元测试。



## 02 环境准备
下面是使用到的工具、软件和环境及其官方链接。

### 测试工程
在 github 获取测试工程 [APM32 Ceedling Example](https://github.com/MorroGeek/apm32-ceedling-example)。

### 工具
如果不想使用命令行,可以使用 vscode 和 Ceedling Test Explorer 插件进行单元测试,以下是一些用到的工具和插件。



- [J-Link Software and Documentation Pack](https://www.segger.com/downloads/jlink/)
- [Visual Studio Code](https://code.visualstudio.com/)
- [Ceedling Test Explorer](https://marketplace.visualstudio.com/items?itemName=numaru.vscode-ceedling-test-adapter)
- [Test Explorer UI](https://marketplace.visualstudio.com/items?itemName=hbenl.vscode-test-explorer)
- [Test Adapter Converter](https://marketplace.visualstudio.com/items?itemName=ms-vscode.test-adapter-converter)
- [Cortex-Debug](https://marketplace.visualstudio.com/items?itemName=marus25.cortex-debug)
- [CMake Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.cmake-tools)

### 依赖
下面是测试工程的依赖,具体的安装配置过程可以参考官方文档,这里就不赘述了。要注意的是,Ceedling 是基于 Ruby 环境的,需要先安装 Ruby 环境。

- [Ruby](https://www.ruby-lang.org/en/)
- [Ceedling](https://github.com/ThrowTheSwitch/Ceedling)
- [CMake](https://cmake.org/)
- [GNU Arm Embedded Toolchain](https://developer.arm.com/tools-and-software/open-source-software/developer-tools/gnu-toolchain/gnu-rm)

## 03 Ceedling 单元测试
Ceedling 是一个工具集合,包含了 CMock、Unity 和 CException 等工具。所以 CMock、Unity 和 CException 的功能也可以在 Ceedling 中使用。下面将一一介绍这些工具的使用方法。

### Ceedling 项目配置
测试工程已经配置好了 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 文件中配置相关参数。参数的详细说明可以参考[官方文档](https://github.com/ThrowTheSwitch/CMock/blob/master/docs/CMock_Summary.md)。

: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 仓库下载。
- [apm32-cmake-example](https://github.com/MorroGeek/apm32-cmake-example)
- [apm32-ceedling-example](https://github.com/MorroGeek/apm32-ceedling-example)

本篇文章就到这里,希望对大家有所帮助。如果有能帮助到大家的地方,欢迎点个小星星。

## 参考资料
- [ThrowTheSwitch/Ceedling: Ruby-based unit testing and build system for C projects (github.com)](https://github.com/ThrowTheSwitch/Ceedling)



使用特权

评论回复

打赏榜单

21小跑堂 打赏了 80.00 元 2024-07-30
理由:恭喜通过原创审核!期待您更多的原创作品~

评论
21小跑堂 2024-7-30 16:28 回复TA
使用Ceedling 单元测试框架,结合 APM32 DAL 库在 vscode 中进行单元测试,测试流程完整,过程详细清晰。测试结果展示较好。 
沙发
kai迪皮| | 2024-7-30 22:10 | 只看该作者

使用特权

评论回复
板凳
xionghaoyun| | 2024-7-31 08:32 | 只看该作者
学习一下

使用特权

评论回复
地板
[鑫森淼焱垚]| | 2024-8-2 15:40 | 只看该作者
学习一下

使用特权

评论回复
发新帖 本帖赏金 80.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

17

主题

25

帖子

3

粉丝