嵌入式代码单元测试如何完成?

strongerHuang 2023-06-03 08:20

关注+星标公众,不错过精彩内容

转自 | 大橙子疯嵌入式

软件开发中,每次需求的变更基本都需要改写代码,而代码变更后就需要进行功能测试,当然在功能测试之前需要代码的单元测试,避免代码改动后部分场景没有验证,最后出现各种问题。

通过测试框架快速完成代码的单元测试,不仅可以覆盖之前测试的场景,也能快速反应问题在哪里

分享正文之前推荐一个嵌入式招聘信息的平台:

常用的C语言测试框架有:

  • Unity:一个小型的,开源的C语言测试框架,提供了用于测试的基本结构和函数。简单好用,常用于嵌入式系统开发。

  • CUnit:一个面向C语言测试的框架,使用简单,支持自动化测试和手动测试。

  • Check:适用于C语言的单元测试框架,使用简单,支持测试套件、测试用例的管理,便于维护测试组件。

  • Google Test:Google推出的C++测试框架,支持C语言,可以跨平台,具有丰富的断言库和Mocks。

  • cmocka:适用于C语言的单元测试框架,支持内存泄漏检测,支持Mock函数和Stub函数等高级用法。

  • criterion:基于C语言的单元测试框架,支持参数化测试和测试用例依赖,具有良好的性能和易用性。

Unity示例

这里介绍Unity,其他的大家感兴趣可以自行查阅,不同的单元测试框架适用于不同的开发需求和场景。开发人员可以按照自己的项目要求选择最适合的框架。

Unity最小可以只用到几个文件即可完成,把Unity源码目录下的unity.cunity.hunity_internals.h三个文件复制至我们的工程目录下进行编译即可,然后在测试文件代码中包含unity.h

https://github.com/ThrowTheSwitch/Unity/releases

简单的示例
完成功能函数的验证

#include 
#include "unity.h"


void setUp() {
    // 这里可以放置每个测试用例运行前的初始化代码
}

void tearDown() {
    // 这里可以放置每个测试用例运行后的清理代码
}

int Add(int a, int b)
{
    return a + b;
}

void test_AddFun(void)
{
    TEST_ASSERT_EQUAL_UINT(6, Add(15));
    TEST_ASSERT_EQUAL_UINT(4, Add(-15));
    TEST_ASSERT_EQUAL_UINT(-6, Add(-1-5));
}


int main()
{
    UNITY_BEGIN();  // 启动测试

    RUN_TEST(test_AddFun);
    UNITY_END();  // 结束测试

    return 0;
}  

通过串口或终端打印内容为:

C:\test/test.c:47:test_AddFun:PASS

-----------------------
1 Tests 0 Failures 0 Ignored
OK

其中,unity_internals.h文件中可以修改输出终端,即UNITY_OUTPUT_CHAR宏的定义

/*-------------------------------------------------------
 * Output Method: stdout (DEFAULT)
 *-------------------------------------------------------*/

#ifndef UNITY_OUTPUT_CHAR
  /* Default to using putchar, which is defined in stdio.h */
  #include 
  #define UNITY_OUTPUT_CHAR(a) (void)putchar(a)
#else
  /* If defined as something else, make sure we declare it here so it's ready for use */
  #ifdef UNITY_OUTPUT_CHAR_HEADER_DECLARATION
    extern void UNITY_OUTPUT_CHAR_HEADER_DECLARATION;
  #endif
#endif


其中自定义实现的C语言扩展库(cot)的容器功能函数都已通过Unity添加了对应的单元测试用例,链接:

https://gitee.com/const-zpc/cot


轻量级通用扩展库

旨在打造一个C语言的通用扩展库。

介绍

  1. 支持多种容器实现,包括通用队列(包括不定长队列)、栈、双向链表和动态数组功能

    双向链表节点可动态创建(需要在初始化分配内存)或静态添加 动态数组在初始化分配的内存中最大限度地使用,支持随机访问(连续地址)

  2. 支持定义序列化/反序列化的结构体功能

    使用到了Boost库中的PP库功能宏语法;确保两边都需要保持头文件结构体定义一致

  3. 移植了部分 C++ Boost库中的PP库功能

    通过宏语法实现复杂的宏语言,灵活进行使用,在编译的时候生成自己期望的代码

软件架构

目录说明

├─cot
│ ├─include
│ │ ├─container // 容器实现头文件
│ │ ├─preprocessor // 移植Boost库中的PP库头文件
│ │ └─serialize // 序列化/反序列化实现头文件
│ └─src
│ ├─container // 容器实现源文件
│ └─serialize // 序列化/反序列化实现源文件
├─test
│ ├─container // 容器实现测试代码
│ └─serialize // 序列化/反序列化测试代码
└─unity // 单元测试框架代码

使用说明

容器类功能使用说明

双向链表使用方式demo

int main()
{
cotList_t list;
cotListItem_t nodeBuf[10];
cotList_Init(&list, nodeBuf, 10);

int data1 = 10;
int data2 = 20;
int data3 = 30;

// 头部增加元素
cotList_PushFront(&list, &data1);

// 尾部增加元素
cotList_PushBack(&list, &data2);

// 插入元素
cotList_Insert(&list, cotList_End(&list), &data3);

// 使用迭代器遍历所有元素
for_list_each(item, list)
{
printf(" = %d\n", *item_ptr(int, item));
}

// 移除指定元素
cotList_Remove(&list, &data3);

// 根据添加移除元素
cotList_RemoveIf(&list, OnRemoveCondition);

cotList_t list2;
cotListItem_t nodeBuf2[3];
cotList_Init(&list2, nodeBuf2, 3);

// 链表内存交换
cotList_Swap(&list1, &list2);

return 0;
}

动态数组使用方式demo

int main()
{
uint8_t buf[20];
cotVector_t vector;

cotVector_Init(&vector, buf, sizeof(buf), sizeof(uint32_t));

// 在尾部追加元素
uint32_t data = 42;
cotVector_Push(&vector, &data);
data = 56;
cotVector_Push(&vector, &data);
data = 984;
cotVector_Push(&vector, &data);

// 插入元素
uint32_t arrdata[2] = {125, 656};
cotVector_InsertN(&vector, 2, &arrdata, 2);

// 删除两个元素
cotVector_RemoveN(&vector, 1, 2);

// 根据添加删除元素
cotVector_RemoveIf(&vector, OnVectorRemoveCondition);

// 打印数组中的数据内容
for (int i = 0; i < cotVector_Size(&vector); i++)
{
printf("%02x ", cotVector_Data(&vector)[i]);
}

return 0;
}

双向队列(定长FIFO)使用方式demo

int main()
{
uint8_t buf[10];
cotQueue_t queue;

cotQueue_Init(&queue, buf, sizeof(buf), sizeof(int));

// 在尾部追加元素
int data = 42;
cotQueue_Push(&queue, &data, sizeof(data));
data = 895;
cotQueue_Push(&queue, &data, sizeof(data));

// 访问元素
int *pData = (int *)cotQueue_Front(&queue);
printf("val = %d \n", *pData);

// 弹出首个元素
cotQueue_Pop(&queue);

return 0;
}

队列(不定长FIFO)使用方式demo

int main()
{
uint8_t buf[10];
cotIndQueue_t queue;

cotIndQueue_Init(&queue, buf, sizeof(buf));

// 在尾部追加元素
char data = 42;
cotIndQueue_Push(&queue, &data, sizeof(data));
int data1 = 80;
cotIndQueue_Push(&queue, &data, sizeof(data1));
long data2 = -400;
cotIndQueue_Push(&queue, &data, sizeof(data2));

// 访问元素
size_t length;
int *pData = (int *)cotIndQueue_Front(&queue, &length);

printf("val = %d \n", *pData, length);

// 弹出首个元素
cotIndQueue_Pop(&queue);

return 0;
}

单向栈使用方式demo

int main()
{
uint8_t buf[10];
cotStack_t stack;

cotStack_Init(&stack, buf, sizeof(buf), sizeof(int));

// 在顶部追加元素
int data = 42;
cotStack_Push(&stack, &data, sizeof(data));
data = 895;
cotQueue_Push(&stack, &data, sizeof(data));

// 访问元素
int *pData = (int *)cotStack_Top(&stack);
printf("val = %d \n", *pData);

// 弹出顶部元素
cotStack_Pop(&stack);

return 0;
}

序列化/反序列化功能使用说明

可以定义一个公共头文件

#ifndef STRUCT_H
#define STRUCT_H

#include "serialize/serialize.h"

COT_DEFINE_STRUCT_TYPE(test_t,
((UINT16_T) (val1) (2))
((INT32_T) (val2) (1))
((UINT8_T) (val3) (1))
((INT16_T) (val4) (1))
((DOUBLE_T) (val5) (1))
((INT16_T) (val6) (1))
((STRING_T) (szName) (100))
((DOUBLE_T) (val7) (1))
((FLOAT_T) (val8) (1))
((STRING_T) (szName1) (100))
)

#endif // STRUCT_H

各个模块引用头文件使用


#include "struct.h"

int main()
{
uint8_t buf[100];

// 序列化使用demo
COT_DEFINE_STRUCT_VARIABLE(test_t, test);

test.val1[0] = 5;
test.val1[1] = 89;
test.val2 = -9;
test.val3 = 60;
test.val4 = -999;
test.val5 = 5.6;
test.val6 = 200;
test.val7 = -990.35145;
test.val8 = -80.699;
sprintf(test.szName, "test56sgdgdfgdfgdf");
sprintf(test.szName1, "sdfsdf");

int length = test.Serialize(buf, &test);

printf("Serialize: \n");

for (int i = 0; i < length; i++)
{
printf("%02x %s", buf[i], (i + 1) % 16 == 0 ? "\n" : "");
}

printf("\n");


// 反序列化使用demo
test_t test2; // COT_DEFINE_STRUCT_VARIABLE(test_t, test2);
COT_INIT_STRUCT_VARIABLE(test_t, test2);

test2.Parse(&test2, buf);

printf("val = %d\n", test2.val1[0]);
printf("val = %d\n", test2.val1[1]);
printf("val = %d\n", test2.val2);
printf("val = %d\n", test2.val3);
printf("val = %d\n", test2.val4);
printf("val = %lf\n", test2.val5);
printf("val = %d\n", test2.val6);
printf("name = %s\n", test2.szName);
printf("val = %lf\n", test2.val7);
printf("val = %f\n", test2.val8);
printf("name = %s\n", test2.szName1);

return 0;
}

声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

------------ END ------------



●专栏《嵌入式工具

●专栏《嵌入式开发》

●专栏《Keil教程》

●嵌入式专栏精选教程


关注公众号回复“加群”按规则加入技术交流群,回复“1024”查看更多内容。




点击“阅读原文”查看更多分享。

strongerHuang 作者黄工,高级嵌入式软件工程师,分享嵌入式软硬件、物联网、单片机、开发工具、电子等内容。
评论
  • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
    虹科Pico汽车示波器 2025-01-23 10:46 80浏览
  • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
    wuliangu 2025-01-21 00:15 324浏览
  • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
    华普微HOPERF 2025-01-20 16:50 123浏览
  • 现在为止,我们已经完成了Purple Pi OH主板的串口调试和部分配件的连接,接下来,让我们趁热打铁,完成剩余配件的连接!注:配件连接前请断开主板所有供电,避免敏感电路损坏!1.1 耳机接口主板有一路OTMP 标准四节耳机座J6,具备进行音频输出及录音功能,接入耳机后声音将优先从耳机输出,如下图所示:1.21.2 相机接口MIPI CSI 接口如上图所示,支持OV5648 和OV8858 摄像头模组。接入摄像头模组后,使用系统相机软件打开相机拍照和录像,如下图所示:1.3 以太网接口主板有一路
    Industio_触觉智能 2025-01-20 11:04 195浏览
  •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
    电子知识打边炉 2025-01-22 11:12 135浏览
  •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
    刘旷 2025-01-21 11:15 666浏览
  • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
    一博科技 2025-01-21 16:17 159浏览
  • 飞凌嵌入式基于瑞芯微RK3562系列处理器打造的FET3562J-C全国产核心板,是一款专为工业自动化及消费类电子设备设计的产品,凭借其强大的功能和灵活性,自上市以来得到了各行业客户的广泛关注。本文将详细介绍如何启动并测试RK3562J处理器的MCU,通过实际操作步骤,帮助各位工程师朋友更好地了解这款芯片。1、RK3562J处理器概述RK3562J处理器采用了4*Cortex-A53@1.8GHz+Cortex-M0@200MHz架构。其中,4个Cortex-A53核心作为主要核心,负责处理复杂
    飞凌嵌入式 2025-01-24 11:21 50浏览
  • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
    丙丁先生 2025-01-21 12:10 619浏览
  • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
    牛言喵语 2025-01-22 17:10 181浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦