迈出代码可测试性的第一步


以下只表示自己此水平下的关于独立开发的一点儿认识,针对团队协作问题复杂度就成倍增加了。建议先看下这篇文章: 你写的程序很健壮?不妨测一下

我对单元测试框架的认识

  前段时间看了一篇文章,里面推荐了两本书。 讲的是测试框架,我第一次看见这个名词是在RT-Thread的组件里,那些组件有些给我眼前一亮的感觉,当发现有个组件是测试框架时,第一想法:这是什么,因为RT-Thread用得较少,就没有机会了解一下这个组件。

  有书可以白嫖,而且认为书的作者应该比那篇文章的号主讲得更清楚明白和系统些,我就好奇性地看了看,果然很吸引人,不知不觉间就看了半本(都是在吊人胃口,《测试驱动的嵌入式C语言开发》)。有兴趣且有时间的可以去试着读一下,说不定觉得发现新大陆了呢。我的认识是它(测试框架,或者叫自动化单元测试框架)能给写代码这个过程加入一个快速反馈

关于代码测试与快速反馈

  之前听罗胖提起过一件事:说一个俄罗斯方块游戏他当时玩了一整天,事后回想起来觉得是这种快速变化快速反馈的循环往复让人沉迷其中。我认为这也适用于写代码。

  软件开发的不变真理:软件总是要变化的。

  对于嵌入式工程师来说应该都曾经有过的感觉,可能写上一天代码也不能测试一回,两三天都有可能,没有测试就没有结果/反馈,就无法调整改进逐渐养成“习得性无助”

  最终调试的时候,可能因为问题来得太多,将全部的精力都放在解决问题上,而不会花不少时间来详细记录问题,当问题全部解决可能又因为新的事注意力被转移了,没有及时做出总结,有些容易解决的问题(不见得是小问题),在下次编程时还会犯。

  我曾经很长一段时间都有的一种忧患/焦虑 意识:我无法保证自己的程序会不会出现下一个bug(可能也和遇到bug后茫然不知从何入手有关),而当产品上线之后,总是担心一个电话被安排到现场找bug。 不过还好的是,可以OTA软件升级来缓解一下。 现在想想,如果代码细化到每一个部分都是自己注入过思考,测试过的。那么就会对自己写的代码有信心,即使遇到问题也知道细化到哪个部分去查找原因。

我理解中有经验的工程师是如何保证代码质量的

  身为一名嵌入式工程师,有经验的可以让自己对代码的掌控力更强一些采用模块化设计,分层架构,有些层(和芯片和硬件有关)的问题只有调试阶段会遇到,绝不会留到产品上线后。但真正的问题还是存在(功能逻辑的问题),比之没有经验的工程师好的一点是可以过滤掉好大一部分代码,再加上模块化设计,问题的定位变得比较容易,容易定位就容易解决

  随着工作经验的上升,编程的逻辑思维越来越强,产生的问题越来越少,甚至对于产品百分之八十的时间也只是在使用百分之二十的功能,把问题压缩在最不常用/都不知道有这功能的那百分之十,百分之五,甚至百分之一。从效果来看,那就是完美的代码。

我认为存在几个不容忽视的问题

  • 这个编程的经验积累时间会很长,长到大多数人都无法承受。
  • 编程的难度需要限定在一定等级一定范围,比如某个公司的产品,在项目需求确定的那一刻起它的编程难度就已经确定了的,而公司一般是做一类产品,难度不存在较大差异。换一家不同行业的公司这种经验的不适用感会让人备受折磨。
  • 这种编程的经验是无法传授的,导致通过自己的努力有可能超过前辈,但无法做到超越前辈。而前辈是公司的中流砥柱,你不是。
  • 这种“完美”的代码只是通过本人和效果来证明,前者存在很强的主观性(我写的代码怎么可能有问题,说出来味道就变了,但大多数人就是这么想的,在出现问题时的态度应该能展现出一二,不是这么想的人就像我曾经焦虑的我一样)。后者证明方式很直接直达目的,但显得有些片面了,多角度全方位观察才能无死角。相比于单个目击者,“罗生门”更有利于还原事件的全貌。

我认为问题的解决方案

  对于前三个问题,根本性的问题并没有意识到,更没有在这方面做出足够的努力。那就是可测试性。**可测试性意味着可“证伪”,然后在此基础上不断改进。**虽然还有很多方法来保证代码质量,最终实现功能且保证稳定性,但可测试性这是绝对绕不过的,还有一个是更为根本和基础的可读性(可读和可读性在我理解中是不一样的,一个是行为表现,一个是根本原则)。

  时间和经验与代码稳定性没有必然的联系,就像“一万小时定律”一样,是个骗局。曾经在某一处看到,“一万小时定律”是某人从一本叫《刻意练习》摘出来的一个词,那个书中举的一个例子是那些四五十年沉浸在象棋里的大师,脑海中记着5~10万张棋谱。最后得出的结论是一万小时的刻意练习,你能成为任何一个领域的大师(大概是这意思)。

  所谓“完美”的代码 对于嵌入式那些开源的“轮子”来说是不存在的,因为源代码是开放的,再加上详细的说明文档(实现细节是完全暴露出来的),而且面对的是一群一大群工程师,里面有不少沉浸在代码中的大神(相当于代码评审),眼尖地很。

可测试带来的快速反馈与bug的关系

  “bug”这个词的起源据说是在第一台计算机中飞进了一只飞蛾,那个东西卡在那儿,引起莫名的运行故障,排查问题花了好长时间,最后发现是这么个小东西,清除出去就运行正常了。然后bug这个词就广为流传。

  莫名运行故障,长时间排查,找到并清除则恢复正常。bug就像突然出现的一个尖峰脉冲,影响正常进度,这个尖峰就是排查和解决时间,可能非常尖(时间耗费相当长)。如果不够尖,只能算是一次正常抖动,那么就不算是bug。而**快速反馈和改进从根本上就能减少bug的产生。如果产生了,那也可以基本忽略掉排查所需消耗的时间**。

怎么用单元测试框架

  本来是打算根据那两本书来开始单元软件测试的,不过由于一些我自己都没意识到的原因,结果就是两本书大致内容都翻了一遍,其中重要的实践部分也尽可能按照书中步骤来了,但还是没有见到一点儿成果。

  从unity 到cpputest 再到googletest,越来越焦虑,本以为网上资料很多随便查查就能上手的,结果没想象中多,也没利用起来。最终打算从那篇文章的作者处找找答案,交流之后发现作者更多的是学习,并没有深入使用。突然感觉自己步子迈得太大了,连初步学习使用都没做好,就想着用到实际工程中。

  后来按照号主那篇文章提到的步骤实践了一遍,由于我用的并不是用的STM32,而是一款蓝牙芯片,两者底层函数不一样,果然出现问题了。

  其中很关键的一步是配置串口输出字符的函数,我调用了那个打印字符的函数,但是最终显示乱码。我严重怀疑就是打印字符的函数有问题两种可能的解决方式:一种方式是找同事协作了解那个打印字符的函数调用方式是否正确;另一种方式就是找到一种自己确认没问题的打印字符的函数。

  最终选择了后者,因为更能说明问题。还是我常用的通过windows命令行来验证的方式,说起来这也该算作可测试性,但是之前的用法太简陋了(只能验证单个函数,或单个功能,没有一种文件的组织架构让我认为可以进行更多的测试;而且只是验证功能逻辑,而不能对其注入测试数据,以及期望值,程序的稳定性完全只是看自己心情(自己的思维方式是否存在漏洞)。不过对于提供一个打印字符的函数(C标准库)和组织多个源文件的编译运行(Makefile)来说,足够我进行验证了。

  在蓝牙芯片上和windows上测试结果如图所示。下面这种才是想要的效果。

image-20210517224557382

image-20210517224533382

整个文件目录如下:

image-20210518092709067

  相比于在代码工程中使用,少了整个工程的文件,多出来了两个文件:Makefile 和 RunCode.bat ,(这些文件的压缩包我上传到了CSDN,有需要的可以看一下test_unity.zip )但最主要的区别还是打印字符的函数(一致的部分还是看原文章比较好,链接我放在了开头,免得我漏掉了什么细节):

image-20210520101353169

//unity_config.h
#ifndef UNITY_CONFIG_H
#define UNITY_CONFIG_H

#ifdef __cplusplus
extern "C"
{
#endif

#include <stdio.h>

#define UNITY_OUTPUT_CHAR(a)    putchar(a)
#define UNITY_OUTPUT_START()    

#ifdef __cplusplus
}
#endif


#endif

image-20210518093059788

那篇文章中是这样的 :

image-20210518093354020

# Makefile
main.exe : leapyear.o unity.o main.o
	gcc leapyear.o unity.o main.o -o main.exe

unity.o : unity.c unity.h unity_internals.h unity_config.h
	gcc -c unity.c -o unity.o

leapyear.o : leapyear.c leapyear.h
	gcc -c leapyear.c -o leapyear.o

main.o : main.c leapyear.h unity.h
	gcc -c main.c -o main.o
	
.PHONY: clean
clean:
	rm -rf *.o main.exe
:: RunCode.bat
make
md output
move *.o %cd%\output
move *.exe %cd%\output
cd %cd%\output
main.exe
pause

  总的来说距离在工程中使用还有很长一段路要走,比如随着文件增多需要一种方式更好地组织编译规则(Makefile),很可能会用到cmake。

我对可测试性的理解

  可测试性是一种原则,它会带来一种反馈过程,通过和其它一些方法辅助,最终目标是写出稳定健壮(好像有个词叫鲁棒性强)的代码。

 可测试性包含但不等同于单元测试,比如还有一种可测试其它问题的软件(比如数组越界),甚至代码评审来从不同工程师的编程角度判断是否有问题(来自他人的意见或建议),还有调试时的问题定位,这些都可以说是反馈。

 单单从单元测试来说,它能改变一个人的编程思维,前提是你习惯了这种编程方式,模块化已经作为编程的根基,要想使用单元测试框架,首先需要列出可测试项,清楚明白每个函数的结果该是什么样的,然后开始行动(编程),以终为始

些都可以说是反馈。

 单单从单元测试来说,它能改变一个人的编程思维,前提是你习惯了这种编程方式,模块化已经作为编程的根基,要想使用单元测试框架,首先需要列出可测试项,清楚明白每个函数的结果该是什么样的,然后开始行动(编程),以终为始

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值