μCUnit,微控制器的单元测试框架
Posted foxclever
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了μCUnit,微控制器的单元测试框架相关的知识,希望对你有一定的参考价值。
在MCU on Eclipse网站上看到Erich Styger在8月26日发布的博文,一篇关于微控制器单元测试的文章,有很高的参考价值,特将其翻译过来以备学习。原文网址:https://mcuoneclipse.com/2018/08/26/tutorial-%CE%BCcunit-a-unit-test-framework-for-microcontrollers/
单元测试是主机开发的常见做法。但对于嵌入式开发,这似乎仍然是一个“空白”领域。主要是因为嵌入式工程师不习惯单元测试,或者因为单元测试的通常框架需要嵌入式目标上的太多资源?
我使用的是μCUnit框架,它是一个小巧易用的框架,面向小型微控制器应用。
uCUnit
框架非常简单:两个头文件和一个.c文件:
uCUnit框架文件
使用uCUnit GitHub站点中的原始站点或使用我从GitHub稍微调整和修改的站点,以与MCUXpresso SDK和IDE一起使用。
概念是单元测试包括提供测试宏的uCunit.h头文件。
头文件中的#define将输出配置为详细或正常:
UCUNIT_MODE_NORMAL或UCUNIT_MODE_VERBOSE
System.c和System.h是系统的连接,主要用于启动,关闭和打印测试结果到控制台。下面是使用printf()方法写入输出的实现,但是这可以被任何写入例程替换或扩展到SD卡上的日志文本。
1 /* Stub: Transmit a string to the host/debugger/simulator */ 2 void System_WriteString(char * msg) { 3 4 PRINTF(msg); 5 6 } 7 8 void System_WriteInt(int n) { 9 10 PRINTF("%d", n); 11 12 }
框架概述
首先,我必须包含单元测试框架头文件:
#include "uCUnit.h"
接着,我必须初始化框架
UCUNIT_Init();
/* initialize framework */
还有一个测试用例包含在UCUNIT_TestcaseBegin()和UCUNIT_TestcaseEnd()中:
UCUNIT_TestcaseBegin(
"Crazy Scientist"
);
/* test cases ... */
UCUNIT_TestcaseEnd();
在最后使用时写一个摘要
UCUNIT_WriteSummary();
如果系统应该关闭使用a
UCUNIT_Shutdown();
测试
该框架提供了多种测试方法,例如:
UCUNIT_CheckIsEqual(x, 0);
/* check if x == 0 */
UCUNIT_CheckIsInRange(x, 0, 10);
/* check 0 <= x <= 10 */
UCUNIT_CheckIsBitSet(x, 7);
/* check if bit 7 set */
UCUNIT_CheckIsBitClear(x, 7);
/* check if bit 7 cleared */
UCUNIT_CheckIs8Bit(x);
/* check if not larger then 8 bit */
UCUNIT_CheckIs16Bit(x);
/* check if not larger then 16 bit */
UCUNIT_CheckIs32Bit(x);
/* check if not larger then 32 bit */
UCUNIT_CheckIsNull(p);
/* check if p == NULL */
UCUNIT_CheckIsNotNull(s);
/* check if p != NULL */
UCUNIT_Check((*s)==’ ’,
"Missing termination"
,
"s"
);
/* generic check: condition, msg, args */
通过几个例子可以解释这一点。
示例:疯狂的科学家
下面是一个‘crazyScientist‘功能,它结合了不同的材料:
1 typedef enum { 2 Unknown, /* first, generic item */ 3 Hydrogen, /* H */ 4 Helium, /* He */ 5 Oxygen, /* O */ 6 Oxygen2, /* O2 */ 7 Water, /* H2O */ 8 ChemLast /* last, sentinel */ 9 } Chem_t; 10 11 Chem_t crazyScientist(Chem_t a, Chem_t b) { 12 if (a==Oxygen && b==Oxygen) { 13 return Oxygen2; 14 } 15 16 if (a==Hydrogen && b==Oxygen2) { 17 return Water; 18 } 19 20 return Unknown; 21 22 }
对此的测试可能如下所示:
1 void Test(void) { 2 Chem_t res; 3 UCUNIT_Init(); /* initialize framework */ 4 5 UCUNIT_TestcaseBegin("Crazy Scientist"); 6 res = crazyScientist(Oxygen, Oxygen); 7 UCUNIT_CheckIsEqual(res, Oxygen2); 8 UCUNIT_CheckIsEqual(Unknown, crazyScientist(Water, Helium)); 9 UCUNIT_CheckIsEqual(Water, crazyScientist(Hydrogen, Oxygen2)); 10 UCUNIT_CheckIsEqual(Water, crazyScientist(Oxygen2, Hydrogen)); 11 UCUNIT_CheckIsInRange(crazyScientist(Unknown, Unknown), Unknown, ChemLast); 12 UCUNIT_TestcaseEnd(); 13 14 /* finish all the tests */ 15 UCUNIT_WriteSummary(); 16 UCUNIT_Shutdown(); 17 }
通过不同的检查,我们可以验证功能是否正在按照我们的预期进行。它产生以下输出:
======================================
Crazy Scientist
======================================
../source/Application.c:60: passed:IsEqual(res,Oxygen2)
../source/Application.c:61: passed:IsEqual(Unknown,crazyScientist(Water, Helium))
../source/Application.c:62: passed:IsEqual(Water,crazyScientist(Hydrogen, Oxygen2))
../source/Application.c:63: failed:IsEqual(Water,crazyScientist(Oxygen2, Hydrogen))
../source/Application.c:64: passed:IsInRange(crazyScientist(Unknown, Unknown),Unknown,ChemLast)
======================================
../source/Application.c:65: failed:EndTestcase()
======================================
**************************************
Testcases: failed: 1
passed: 0
Checks: failed: 1
passed: 4
**************************************
System shutdown.
我建议在执行之前编写单元测试*,因为这样我就可以考虑所有不同的极端情况并改进要求。
以上输出设置为UCUNIT_MODE_VERBOSE。使用UCUNIT_MODE_NORMAL,它使用更紧凑的格式并仅打印失败的测试:
======================================
Crazy Scientist
======================================
../source/Application.c:63: failed:IsEqual(Water,crazyScientist(Oxygen2, Hydrogen))
======================================
../source/Application.c:65: failed:EndTestcase()
======================================
**************************************
Testcases: failed: 1
passed: 0
Checks: failed: 1
passed: 4
**************************************
System shutdown.
跟踪点
在上面的例子中,我们只是从外部测试函数的功能。如何检查以下函数中的测试确实检查除以零的情况?
1 int checkedDivide(int a, int b) { 2 if (b==0) { 3 PRINTF("division by zero is not defined! "); 4 return 0; 5 } 6 return a/b; 7 }
要检查是否真的输入了if()条件,我可以添加一个跟踪点。跟踪点的数量在μCUnit.h中配置为:
/**
* Max. number of checkpoints. This may depend on your application
* or limited by your RAM.
*/
#define UCUNIT_MAX_TRACEPOINTS 16
和
UCUNIT_ResetTracepointCoverage();
我可以重置跟踪点。
我用跟踪标记执行跟踪点(在0..UCUNIT_MAX_TRACEPOINTS-1范围内)
UCUNIT_Tracepoint(id);
和
UCUNIT_CheckTracepointCoverage(0);
我可以检查是否触摸了给定的跟踪点。在要测试的功能下面有一个跟踪点:
1 int checkedDivide(int a, int b) { 2 if (b==0) { 3 UCUNIT_Tracepoint(0); /* mark trace point */ 4 PRINTF("division by zero is not defined! "); 5 return 0; 6 } 7 return a/b; 8 }
相应的单元测试代码:
1 UCUNIT_TestcaseBegin("Checked Divide"); 2 UCUNIT_CheckIsEqual(100/5, checkedDivide(100,5)); 3 UCUNIT_ResetTracepointCoverage(); /* start tracking */ 4 UCUNIT_CheckIsEqual(0, checkedDivide(1024,0)); 5 UCUNIT_CheckTracepointCoverage(0); /* check coverage of point 0 */ 6 UCUNIT_TestcaseEnd();
然后生成:
======================================
Checked Divide
======================================
../source/Application.c:69: passed:IsEqual(100/5,checkedDivide(100,5))
division by zero is not defined!
../source/Application.c:71: passed:IsEqual(0,checkedDivide(1024,0))
../source/Application.c:72: passed:TracepointCoverage(1)
字符串测试
还有许多其他方法可以使用检查,最多可以使用用户配置的检查和消息。以下是要测试的函数的示例:
1 char *endOfString(char *str) { 2 if (str==NULL) { 3 return NULL; 4 } 5 while(*str!=‘