嵌入式,真的不需要单元测试?

嵌入式大杂烩 2020-10-21


前言

嵌入式行业摸爬滚打这几年,遇见有规范单元测试的项目寥寥无几。归根到底,无非是公司希望快速迭代出产品,有问题等客户反馈再说。当然,也有人认为是嵌入式行业都是小而美的产品居多,没有到一定量级之前,玩不起单元测试这种配置。正如做个蛋炒饭,并不需要安排主厨、二厨一般

不过出于对代码稳定性的追求,我认为还是应该着手了解一下单元测试的。毕竟,这是有效提高代码说服力的方式之一。

相信没有真正体验过单元测试好处的读者一看到"单元测试"这几个字,可能会出现以下两种反应之一:

  • 由于没有单元测试的经验,因此对采用这一方法去保证软件质量很好奇,也迫切地想要了解这一方法在项目中的实施

  • 曾经使用单元测试但效果不好,因为在嵌入式行业,时常要跟硬件打交道,单元测试很难检测硬件问题,所以往往一看到"单元测试"这几个字的反应就是"没用"

如果读者是第一种反应那很好,本文就是科普单元测试的基本要点。如果读者是第二种反应,那可能是对单元测试存在偏见,本系列文章也会介绍mock测试、错误注入等方式,使单元测试也能合理使用于嵌入式行业。



01

单元测试真的"无用"?



造成"单元测试无用论"的第一个原因是,运用这一方法的时机不恰当。不少项目在一开始真正关心质量的人很少,更谈不上采用一整套的方法论去保证质量了。产品在开发出来后发现到处存在问题,只会拆西墙补东墙根本就不能阻止问题一而再,再而三地出现。于是,开始想起单元测试。一声令下,整个项目开始做单元测试。单元测试以模块为单位,需要先把项目拆分出来。如果你的项目代码整体耦合程度较高的话,单元测试根本无从说起,拆分的工作会让你痛苦不已。

单元测试是一项耗时的工作,但管理者却往往希望在短期内看到效果。或者单元测试还没做到位管理层就等不及了,催你马上开始下一步的开发,结果只能是前功尽弃。正确的做法是:在项目的开始之初就引入单元测试。对于以前没有部署单元测试的项目,先只对新增加的、相对独立的模块做单元测试、并逐渐覆盖老代码。

第二个导致"单元测试无用论"的原因是,方法没有运用到位。要保证单元测试的有效性一定要引入另一个概念--代码覆盖。关于代码覆盖,我以后会另外再写一篇文章介绍。只有将单元测试和代码覆盖结合在一起,综合使用才能保证单元测试的效果。

02

最原始的"单元测试"



这里给读者展示一下,不使用任何单元测试框架时,是怎么做单元测试的。

下面简单以linux内核链表为例:

struct list_head { struct list_head *next, *prev;};/*定义一个结构体,只含有表示前驱和后继的指针,它就是我们的主角了*/#define LIST_HEAD_INIT(name) { &(name), &(name) }/*静态初始化*/#define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name)/*动态初始化*/static inline void INIT_LIST_HEAD(struct list_head *list){ list->next = list; list->prev = list;}/*插入操作*//*删除操作*//*合并操作*/...

完整代码很长,这里没有必要全部贴出,能起演示作用就足够了。

现在就以INIT_LIST_HEAD函数为例,来考虑如何为这个函数设计测试用例。INIT_LIST_HEAD函数的实现是如此的简单,以至于很容易让人觉得为它设计单元测试是多余的。但是,从单元测试的角度看,只要不存在可行性问题就不应考虑因为简单而不对其进行验证。而且,放弃对之进行验证,以后会降低代码覆盖率。

做单元测试需要通过编写程序的方式来完成,所编写的用于测试的代码又称为单元测试用例。

下面我们来简单实现一个INIT_LIST_HEAD函数的测试用例:

int main(int argc,char **argv){ struct list_head list;
/*避免函数没有使用参数而引发waining*/ UNUSED(argc); UNUSED(argv);
list.prev = (struct list_head*)0xaaaa; list.next = (struct list_head*)0xbbbb;
INIT_LIST_HEAD(list);
/*检查前指针*/ if(list.prev != list){ return -1; }
/*检查后指针*/ if(list.next != list){ return -1; }
return 0;}

这应该是史上最简单的测试用例,功能非常简单,首先是故意将list结构体中的各个指针变量初始化为一个随机值。然后在调用完INIT_LIST_HEAD函数之后,检查各成员是否被初始化为了list,以判断INIT_LIST_HEAD函数是否正常工作了。注意:这个测试程序还有一个约定,返回-1代表测试失败,返回0表示测试成功。

这个测试用例是基于我们对INIT_LIST_HEAD函数有足够的了解之后编写的,这种测试方法在软件测试领域有个正儿八经的名字,叫白盒测试。

相信通过这个NIT_LIST_HEAD函数的测试用例,你已经初步建立起了对单元测试的印象。但是千万不要以为单元测试仅此而已,这是我刻意简化的结果。要完整地掌握单元测试,还要好好学习一段时间。

目前,对于这个小小的单元测试案例,还有很多的不足,下面简单罗列了几项:

  • 如果对于每一次检查都采取直接写if语句的形式,将造成大量的冗余代码,并且测试用例的编写效率也会很低。

  • 通过观察程序是否返回0或者是-1的方式来判断所有的测试是否通过并不直观,一旦出错也无法马上判断是那一步测试出了问题。毫无疑问,我们需要更加直观的方式来展示哪一步成功或者哪一步失败。

  • 一份严谨的测试用例,会有大量的判定。如果一个测试程序存在100次判定,其中出现了3次失败,那最终显示一个百分比的测试通过率会比较直观,比如可以显示97%的测试成功了。

后面会进一步介绍如何自己搭建一个简单实用的单元测试框架,来解决上面这些问题。也会陆续展开介绍mock方法、打桩、错误注入、代码覆盖、动态分析、静态分析、性能优化等内容。

03

总结


正如很多其他技巧,比如打桌球、滑雪一样,测试驱动开发也要花费相当长时间来练习。许多开发者已经接受了这种技术,而且再也不想回到从前“后期调试式编程”的方式去了。

它会使你的代码:

  •  产生的bug更少

  • 调试时间更短

  • 完全可以通过提交你的单元测试案例,来证明你的项目可靠性。



猜你喜欢

干货 | 函数宏的三种封装方式

你写的程序很健壮?不妨测一下?


1024G 嵌入式资源大放送!包括但不限于C/C++、单片机、Linux等。在公众号聊天界面回复1024,即可免费获取!

嵌入式大杂烩 专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!
评论
热门推荐
相关推荐
我要评论
0
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦