《测试驱动的嵌入式C语言开发》——2.2节Unity:一个全部用C实现的自动化测试框架

2.2 Unity:一个全部用C实现的自动化测试框架
Unity是一个简单且直接的自动化单元测试框架。它由很少的几个文件构成。让我们通过几个示例单元测试用例来认识一下Unity和单元测试。如果你是一个长期的Unity用户,你会发现如果不用Unity所提供的脚本来生成测试运行容器,那么额外的几个宏可能会很有帮助。
用Unity写的sprintf()测试用例
测试要写得短并且有重点。可以把它想象成一个安检过程,通过时保持安静,失败时就要发出警报声。下面这个测试让sprintf()处理一个没有格式操作的格式声明。

宏TEST()用来定义一个测试函数,当运行所有测试的时候就会调用它。第一个参数是一个测试的组名。第二个参数是这个测试的名字。我们将在第3章中更仔细地介绍TEST()这个宏。
宏TEST_ASSERT_EQUAL()用来比较两个整数。sprintf()应该返回给测试说它格式化的结果字符串长度为3,并且如果是这样的,TEST_ASSERT_EQUAL()就成功了。像大多数的自动化测试框架一样,TEST_ASSERT_EQUAL()的第一个参数是期望中的值。
TEST_ASSERT_EQUAL_STRING()用来比较两个以null结尾的字符串。这句话是声明output的内容必须是字符串“hey”。按照习惯,第一个参数是期望值。
如果任意一个检查的条件不满足,测试就会失败。这些检查会按顺序进行,TEST()会在碰到第一个失败时终止。
注意,TEST_ASSERT_EQUAL_STRING()可能在意外的情况下通过。如果output的内容刚好本来就是“hey”,那么如果sprintf()什么也不干,这个测试也会通过。当然,这不大可能发生,但我们最好还是改进一下这个测试,把output初始化成一个空字符串。

如果我们担心sprintf()会破坏output之前的内存,我们也可以把output的大小再加一个字符,然后把&output[1]传给sprintf()。最后检查一下output[0]是否还是0xaa,这在很大程度上能证明sprintf()的行为是正确的。
在C语言中,要让测试面面俱到很难。错误的或是恶意的代码可以在output之后或之前距离很远的地方搞破坏。把测试做到什么程度是需要我们自己来判断的。后面你将会看到在TDD中如何决定要写哪些测试。
在以上的测试中,你可以看到其中正在滋生一些细微的重复。output的声明是重复的,初始化是重复的,以及对写过界的检查也是重复的。如果只有两个测试,可能问题不大,但如果你刚好是维护sprintf()的人,你需要比这多得多的测试。随着测试的增加,这些重复会越来越多并且让测试用例中的代码难以理解。接下来让我们看一看测试夹具是如何帮我们避免TEST()测试用例中的重复的。
Unity中的测试夹具
使用测试夹具的目的是避免重复。测试夹具把所有测试都需要的那些部分组织在一处。请留意以下对sprintf()测试的代码中是如何使用TEST_SETUP()和TEST_TEAR_DOWN()来避免重复的。

在TEST_GROUP()之后定义的共享数据会在TEST_SETUP() 中进行初始化,TEST_SETUP() 将在每个TEST()的起始大括号之前执行。这些数据在源文件内可见,而且可以被源文件内的TEST() 和辅助函数使用。在我们的这个TEST_GROUP()组中不需要在TEST_TEAR_DOWN()中做任何清理工作。
作用域为源文件的辅助函数expect()和 given()会帮助sprinitf()的测试保持整洁和不重复。
说到底,这就是普通的C代码,所以你可以任意使用共享数据和辅助函数。我给出的是一种典型的结构,用于有共享数据和检查条件的一组测试。
现在这些测试更专注、简约、直接、能说明问题。

你会发现,一旦你理解了一个特定的TEST_GROUP()并且看过了几个例子以后,要想写出下一个测试用例会简单很多。当一个TEST_GROUP() 中有一个共同的模式时,每个测试都变得很容易阅读、理解和拓展。
安装Unity测试
从前面的例子中很难看出测试用例是如何运行并使用必要的前置和后置过程的,而这是通过另一个叫TEST_GROUP_RUNNER()的宏来完成的。TEST_GROUP_RUNNER()可以和测试放在一起,也可以放在不同的文件里。为了避免总是要在同一文件里上下滚动,本书使用了不同的文件。对于sprintf()的两个测试,TEST_GROUP_RUNNER()看起来如下所示:

每个测试用例都通过RUN_TEST_CASE()宏来调用。基本上就相当于RUN_GROUP_RUNNER()会依次调用以下这些函数体:

RUN_TEST_GROUP(测试组名)会调用由TEST_GROUP_RUNNER()定义的函数。所以每个你希望main()来运行的TEST_GROUP_RUNNER()都要加上一个RUN_TEST_GROUP。请注意,一定要把RunAllTests()传给UnityMain()函数。
使用纯C实现的测试框架比较遗憾的一点是,你要记得把每个TEST()加到TEST_GROUP_RUNNER()中,并且整个容器将被UnityMain()函数调用。如果你忘了,测试会被编译但不会执行,可能出错了你也不会知道。
因为存在这种可能出错的情况,所以Unity设计者创建了一个代码生成系统,可以读取你的测试文件并生成测试容器代码。为了让刚开始使用时减少对Unity的依赖,我将不会使用代码生成脚本,而是手工把测试代码串联起来。
在下一节我们介绍CppUTest时,你会看到这个问题的另一种解决办法。在这之前先让我们来看看Unity的输出。
Unity的输出
测试的运行应该作为测试自动构建的一部分。应该用一个简单的命令就可以构建并运行构建出来的测试可执行程序。你会发现我经常构建,每次小的改动后都会重新构建。这就是TDD。我把我的开发环境设置成每次我保存一个文件时就会自动构建所有的东西。测试的输出就像这样:

请注意,在所有测试都通过时,输出最少。随便瞟一眼,如果看到一行写着“OK”的文字,这意味着“所有测试通过”。这符合UNIX风格,测试框架遵循“没有消息就是最好的消息”原则(稍后你会看到,当有测试失败时它会给出特定的错误信息)。它看上去很直白,但还是让我们来看看每行输出都是什么含义。
请注意,每个测试用例运行时都会打印出一个点(.)。如果测试运行起来时间很长,这会让我们感知到有东西在运行。分隔符(---)那一行只是用来分隔测试的总结。
Tests—总共的TEST()个数。
Failures—失败的TEST()的个数。
Ignored—处在忽略状态的测试的个数。忽略的测试会被编译但不会执行。
让我们加一个出错的测试来看看会发生什么。看看测试的输出,我们故意加的错误应该很明显:

失败信息会报告失败的测试用例所在的文件名和行号、测试用例的名字,以及为什么会失败。并且请留意最后的总结中显示一个测试失败了。
你可以在附录B中找到更多关于Unity的信息。

时间: 2024-09-30 14:36:20

《测试驱动的嵌入式C语言开发》——2.2节Unity:一个全部用C实现的自动化测试框架的相关文章

《测试驱动的嵌入式C语言开发》——导读

目 录 第1章 测试驱动开发1.1 为什么我们需要TDD1.2 什么是测试驱动开发1.3 TDD的机理1.4 TDD的微循环1.5 TDD的好处1.6 对于嵌入式开发的益处第一部分 开 始第2章 测试驱动开发的工具和约定2.1 什么是自动化单元测试框架2.2 Unity:一个全部用C实现的自动化测试框架2.3 CppUTest:一个用C++实现的自动化单元测试框架2.4 单元测试也会崩溃2.5 "四阶段"模式2.6 我们到哪里了第3章 开始一个C语言模块3.1 具有可测性的C模块的那些

《测试驱动的嵌入式C语言开发》——第1章测试驱动开发

第1章 测试驱动开发我们都做过这样的事--写一大堆代码然后艰难地使它工作起来.也就是先建造再修正.测试是在代码写完之后的事情.测试总是一件后面加上来的事情,这也是我们过去唯一所知的方法.这种很难预料的过程被亲切地称为"调试"(debugging),我们可能会在其中花掉半个小时.调试的过程在我们的进度中被"测试"和"集成"粉饰起来.它总是风险和不确定的来源.修改一个bug可能导致产生另一个,有时是一系列其他的bug.我们往往会统计这些数据来预测把b

《测试驱动的嵌入式C语言开发》——2.3节CppUTest:一个用C++实现的自动化单元测试框架

2.3 CppUTest:一个用C++实现的自动化单元测试框架现在你已经见过了Unity,接下来我会快速介绍一下CppUTest,同时也是我更倾向于使用的对C和C++代码进行单元测试的自动化测试框架.事实上,不仅因为它是一个功能全面的测试框架,同时也因为我是CppUTest的作者之一.本书开始的几个例子会用Unity,在第8章之后会使用CppUTest.CppUTest是为了支持在多种操作系统上开发嵌入式软件而特别设计的.CppUTest的宏被设计成不需要了解C++也可以写测试用例.这使得C程序

《测试驱动的嵌入式C语言开发》——第3章开始一个C语言模块

第3章 开始一个C语言模块在本章里,我会带你浏览用测试驱动来开发一个新的C模块首先要经历的那些步骤.在第4章里,我们则会全速前进来完成这个模块.从这一章开始并且贯穿本书,我们会关注到底能不能实现Dijkstra的不引入bug的愿景.我们所用的工具就是TDD.

《测试驱动的嵌入式C语言开发》——3.5节先测试驱动接口再测试驱动内部实现

3.5 先测试驱动接口再测试驱动内部实现好的接口对于设计良好的模块来讲很关键.前面几个测试会驱动接口设计.关注于接口意味着我们是从外向内开发代码的.测试作为接口的首个用户,从调用者(或客户端代码)的角度给出了开发代码的使用方式.从使用者的角度出发会产生可用性更强的接口.我通常也会让前面的几个测试来检验一些产品代码的边界条件.选择一个带边界检查的简单用例. 为了消除这个编译错误,在模块的接口声明头文件中增加这个接口函数原型: 写出并且通过这些测试能帮助我们实现以下目标:它定义了驱动程序的一个接口函

《测试驱动的嵌入式C语言开发》——3.3节写一个测试列表

3.3 写一个测试列表在开发新功能之前先创建一个测试列表会很有帮助.测试列表由需求衍生而来.测试列表定义了你对将需要完成的功能的最好的理解.这个列表不需要很完美.它只是个临时的文档,可能只记在一张卡片上或者笔记本上.你也可以直接把它当做注释输入到测试文件中.随着每个测试的添加,对应的注释将被删除.不要在写作这个列表上花太多时间,对于LED驱动程序来讲,花几分钟即可.我的初始测试列表如图3-1所示.在我们开始之前,先列出我们都需要测试什么. 请当心在创建测试列表时的报酬递减(diminishing

《测试驱动的嵌入式C语言开发》——2.1节什么是自动化单元测试框架

2.1 什么是自动化单元测试框架自动化单元测试框架就是一个软件包,它能让程序员表达产品代码应该有什么样的行为.自动化单元测试框架的工作就是要提供以下能力: 用于表述测试用例的通用语言: 用于表述期望结果的通用语言: 能够使用产品代码所用编程语言的功能: 能把所有的工程.系统或子系统中的单元测试用例收集到一起: 一个能运行全部或者部分测试用例的机制: 对于测试套件的成功和失败给出明确的报告: 对于失败的测试给出详细的报告.本书中用到的两个单元测试框架在测试嵌入式C代码以及开源代码中都很流行,并且它

《测试驱动的嵌入式C语言开发》——2.6节我们到哪里了

2.6 我们到哪里了到这里,你应当对Unity和CppUTest的概况有了很好的了解,并且明白了如何用测试夹具和测试用例来定义测试.到目前为止你还没有看到测试驱动开发(TDD),为sprintf()写的那些测试并不是TDD,因为sprintf()是已经存在的代码.请你把新学到的知识运用到接下来的练习中.在接下来的两章中我们会用测试驱动来开发一些新代码.学以致用 在开发平台上建立一个测试环境.你可以从附录A中得到一些帮助.下载书中的代码,并运行makefile.可以访问本书的主页找到书中的代码:

《测试驱动的嵌入式C语言开发》——3.10节学以致用

学以致用 重新编写一个你自己的LedDriver,以便在第4章中使用.你可以在code/SandBox中找到开始所需的文件.其中有一个README.txt文件可以参考. 为一个内容为整型的先入先出的环状缓冲CircularBuffer写一个测试列表. 开始测试驱动开发CircularBuffer.选择检查初始状态并使用其接口的那些测试来做TDD.选择那些可以只用硬编码返回值就能通过的测试.你会需要修改makefile让它能够找到CircularBuffer的文件.在第4章之后你将会有机会完整地实