《测试驱动的嵌入式C语言开发》
Unity介绍
Unity用于C的单元测试(特别是嵌入式软件)[UNIT TESTING FOR C (ESPECIALLY EMBEDDED SOFTWARE)].
Unity本身使用纯C代码编写的,遵循ANSI标准,同时支持大多数的嵌入式编译器。Unity可为64处理器进行单元测试,同样也可以为8位MCU进行单元测试。Unity旨在帮助您充分利用您的测试套件,它具有丰富的断言,因此您可以找到满足您需求的完美匹配。Unity很容易集成到您的工具链中,从基础开始,然后使用可选脚本或者功能启动。Unity很容易扩展,可以通过共用件新的宏以适合您自己的自定义类型。可通过CMock工具获得完整的模拟支持,编写流程或结果的脚本,是Unity拥有无限可能。
Unity如何工作
Unity显然是关于断言(assert)的。Unity所有的断言可到https://github.com/ThrowTheSwitch/Unity/blob/master/docs/UnityAssertionsCheatSheetSuitableforPrintingandPossiblyFraming.pdf下查看,但是更建议直接把https://github.com/ThrowTheSwitch/Unity整个工程下载下来,后面会很依靠这个工程里面的内容,断言在嵌入式系统中我们总希望其结果是true。断言最基本的样式是这样的:
int a = 1;TEST_ASSERT( a == 1 ); //this one will passTEST_ASSERT( a == 2 ); //this one will fail
你可以使用上面的TEST_ASSERT,你几乎可以测试你的C代码可以处理的任何东西......但是当出现问题时,你会看到类似这样的东西:
TestMyModule.c:15:test_One:FAIL
这样运行的结果虽然正确,但是输出的信息并没有什么值得参考的价值。有两种方式可以解决这个问题,蛮力方法(适用于非标准case情况):
TEST_ASSERT_MESSAGE( a == 2 , "a isn't 2, end of the world!");
这样输出的结果将会变为:
TestMyModule.c:15:test_one:FAIL:a isn't 2, end of the world!
然后是优雅的解决方案,使用Unity的众多漂亮断言:
TEST_ASSERT_EQUAL_INT(2, a);TEST_ASSERT_EQUAL_HEX8(5, a);TEST_ASSERT_EQUAL_UINT16(0x8000, a);
这样,如果单独运行测试的话,将会输出以下失败信息:
TestMyModule.c:15:test_One:FAIL:Expected 2 was 1TestMyModule.c:23:test_Two:FAIL:Expected 0x05 was 0x01TestMyModule.c:31:test_Three:FAIL:Expected 32768 was 1
这样测试输出的信息就很有用了。其中第一个参数是预期值,第二个参数是您正在测试的值。他以一种对您来说最方便的格式清晰的打印出来。实际上,Unity可以处理各种类型,而不仅仅是整数。如:
TEST_ASSERT_EQUAL_FLOAT( 3.45, pi );TEST_ASSERT_EQUAL_STRING( "Attention, Dr. Surly", greeting );
它甚至可以处理您想要添加的自定义消息的情况,您想要检查数据是否Full,或者即想检查数组是否Full并且输出测试信息。如:
TEST_ASSERT_EQUAL_INT_ARRAY( expArray, actualArray, numElements );TEST_ASSERT_EQUAL_INT_MESSAGE( 5, val, "Not five? Not alive!" );TEST_ASSERT_EQUAL_INT_ARRAY_MESSAGE( e, a, 20, "Oh snap!" );
在每个测试中会使用一个或多个类似的断言。测试只是一个不带参数的C函数。不返回任何内容。按照惯例。它以"test"或者"spec"开头(我将会使用test作为开头):
void test_FunctionUnderTest_should_ReturnFive(void) {TEST_ASSERT_EQUAL_INT( 5, FunctionUnderTest() );TEST_ASSERT_EQUAL_INT( 5, FunctionUnderTest() ); //twice even!}
单个测试文件通常会有多个测试。通常,一个测试文件用于测试相应C源文件的所有方面。这可以通过简单的命名约定最清楚:在哪里可以找到MadScience.c的测试?当然,在TestMadScience.c中!
使用Unity进行单元测试
先给出我学习该部分内容的源码https://github.com/LOVEELEC/xUnity/tree/master/Unity该链接对应的是所有Unity学习过程中所用到的全部内容(随着学习的不断进行会不断添加新的内容,同时也有可能会调整原有内容,调整的内容我会尽量在文档中修改同步内容)。该部分使用的的是DumbExample对应的代码。
关于Unity测试工程结构可以参考https://www.udemy.com/unit-testing-and-other-embedded-software-catalysts/中的The Flow对应的视频,有兴趣的小伙伴也可以购买该课程进行学习。
该部分程序我采用的是最简单的结构,每个proj包含src、test及build文件,同时把对应的Makefile文件也放到同一目录方便编译。其中src包含的被测代码、test中包含的是测试代码、build中包含的编译后的输出内容。具体Makefile中包含的内容我就不献丑了直接给出官方的说明链接http://www.throwtheswitch.org/build/make。
假设我们要测试一个名为DumbExample.c的C文件。它看起来像这样:
#include“DumbExample.h”int8_t AverageThreeBytes(int8_t a,int8_t b,int8_t c){return(int8_t)(((int16_t)a +(int16_t)b +(int16_t)c)/ 3);}
它有一个头文件,如下所示:
#include <stdint.h>int8_t AverageThreeBytes(int8_t a,int8_t b,int8_t c);
然后我们制作一个测试文件TestDumbExample.c,它检查一些基本的东西,如翻转和诸如此类的东西:
#include "unity.h"#include "DumbExample.h"void test_AverageThreeBytes_should_AverageMidRangeValues(void){TEST_ASSERT_EQUAL_HEX8(40, AverageThreeBytes(30, 40, 50));TEST_ASSERT_EQUAL_HEX8(40, AverageThreeBytes(10, 70, 40));TEST_ASSERT_EQUAL_HEX8(33, AverageThreeBytes(33, 33, 33));}void test_AverageThreeBytes_should_AverageHighValues(void){TEST_ASSERT_EQUAL_HEX8(80, AverageThreeBytes(70, 80, 90));TEST_ASSERT_EQUAL_HEX8(127, AverageThreeBytes(127, 127, 127));TEST_ASSERT_EQUAL_HEX8(84, AverageThreeBytes(0, 126, 126));}int main(void){UNITY_BEGIN();RUN_TEST(test_AverageThreeBytes_should_AverageMidRangeValues);RUN_TEST(test_AverageThreeBytes_should_AverageHighValues);return UNITY_END();}
所以我们有一个包含两个测试的测试文件。每个测试都有多个断言。如果这些断言中的任何一个失败,那个特定的测试应该失败,我们应该继续进行下一个测试。完成后,它应该输出我们的结果。
接下来让我们将目录切换到~/TDD/xUnity/Unity/DumbExample:
loveelec@ubuntu:~$ cd ~/TDD/xUnity/Unity/DumbExample/loveelec@ubuntu:~/TDD/xUnity/Unity/DumbExample$ lsbuild Makefile src testloveelec@ubuntu:~/TDD/xUnity/Unity/DumbExample$ make-----------------------IGNORES:----------------------------------------------FAILURES:----------------------------------------------PASSED:-----------------------test/TestDumbExample.c:21:test_AverageThreeBytes_should_AverageMidRangeValues:PASStest/TestDumbExample.c:22:test_AverageThreeBytes_should_AverageHighValues:PASSDONE
接下来让我们看看运行效果:
loveelec@ubuntu:~/TDD/xUnity/Unity/DumbExample$ cd build/loveelec@ubuntu:~/TDD/xUnity/Unity/DumbExample/build$ lsdepends objs results TestDumbExample.outloveelec@ubuntu:~/TDD/xUnity/Unity/DumbExample/build$ ./TestDumbExample.outtest/TestDumbExample.c:21:test_AverageThreeBytes_should_AverageMidRangeValues:PASStest/TestDumbExample.c:22:test_AverageThreeBytes_should_AverageHighValues:PASS-----------------------2 Tests 0 Failures 0 IgnoredOKloveelec@ubuntu:~/TDD/xUnity/Unity/DumbExample/build$
该部分代码只是为了熟悉Unity的功能,并没有按照TDD开发流程进行(先写测试代码再写被测代码)。
代码解析
代码从main入口, UNITY_BEGIN()及UNITY_END()标识测试代码的开始与结束,每个测试均需要用RUN_TEST进行安装,否则将会存在所写的测试代码只会被编译却不会运行测试的问题。该章节完全取自官网内容。详情请参见http://www.throwtheswitch.org/unity。