[开发工具]基于 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)。
---------------------
作者:susutata
链接:https://bbs.21ic.com/icview-3393196-1-1.html
来源:21ic.com
此文章已获得原创/原创奖标签,著作权归21ic所有,任何人未经允许禁止转载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值