μ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)==’\0’,
"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!\n"); 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!\n"); 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!=‘\0‘) { 6 str++; 7 } 8 return str; 9 }
使用以下測試代碼:
1 UCUNIT_TestcaseBegin("Strings"); 2 UCUNIT_CheckIsNull(endOfString(NULL)); 3 str = endOfString("abc"); 4 UCUNIT_Check( 5 (str!=NULL), /* condition to check */ 6 "string shall be not NULL", /* message */ 7 "str" /* argument as string */ 8 ); 9 UCUNIT_CheckIsEqual(‘\0‘, *endOfString("")); 10 UCUNIT_CheckIsEqual(‘\0‘, *endOfString("hello")); 11 str = endOfString("world"); 12 UCUNIT_CheckIsNotNull(str); 13 UCUNIT_CheckIsEqual(‘\0‘, *str); 14 UCUNIT_TestcaseEnd();
其輸出:
======================================
Strings
======================================
../source/Application.c:76: passed:IsNull(endOfString(NULL))
../source/Application.c:82: passed:string shall be not NULL(str)
../source/Application.c:83: passed:IsEqual(‘\0‘,*endOfString(""))
../source/Application.c:84: passed:IsEqual(‘\0‘,*endOfString("hello"))
../source/Application.c:86: passed:IsNotNull(str)
../source/Application.c:87: passed:IsEqual(‘\0‘,*str)
概要
μCUnit是一個非常簡單但功能強大的嵌入式設備和微控制器單元測試框架。它易於使用,只需要極少的資源,並通過自動化單元測試幫助提高嵌入式軟件的質量。我希望你也覺得它很有用。
鏈接
- μCUnit網頁:http://www.ucunit.org/
- μCUnit文檔:http://www.ucunit.org/_documentation.html
- μCUnitGithub網站:https://github.com/ucunit/ucunit
- μCUnit示例用法:https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/FRDM-K64F/FRDM-K64F_uCUnit
- 適用於MCUXpresso的μCUnit端口:https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/MCUXpresso/FRDM-K64F/FRDM-K64F_uCUnit/uCUnit
歡迎關註:
μCUnit,微控制器的單元測試框架