1. 程式人生 > >μCUnit,微控制器的單元測試框架

μCUnit,微控制器的單元測試框架

abc 價值 替換 是否 ear 用戶配置 mode 一起 set

  在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,微控制器的單元測試框架