嵌入式測試驅動開發
DESCRIPTION
《 Test-Driven Development for Embedded C 》心得分享。 TDD(測試驅動開發)是任何開發人員應該掌握的編程實踐,開發者依照需求設計單元測試,然後編寫程式滿足測試,在快速密集的回饋循環中逐漸完善功能,並隨時維持良好的軟體品質。這種開發方式對於物件導向語言陣營的朋友來說應該不陌生,但由於開發環境的特性,使用程序語言的嵌入式平台開發者可能壓根沒聽過或者自認今生無緣。 希望這次交流能為嵌入式平台開發者介紹一些不同於以往的開發方式,打開每個通往敏捷軟體開發的可能。分享內容包含嵌入式TDD原理與策略,單元測試相關工具,如何斷開模組依賴關係,如何得到可測試的設計,以及實務上的建議。TRANSCRIPT
嵌入式測試驅動開發
Hugo
2/13/2014
熟悉的開發方式
先寫程式,再寫測試 bug
Test-After Development (TAD)
問題是... 沒出錯不知道哪裡有 bug
bug 小時候放著不管
長大很恐怖
有什麼方法... 能盡早把 bug 出來?
先寫測試 bug,再寫程式
Test-Driven Development (TDD)
Test Driven Development 測試驅動開發
TDD 循環
加 紅燈:增 一個運行失敗,甚至無法 譯的測試
加 綠燈:快 修改, 做能讓測試 過的工作
加 黃燈:重構,移除重複改進代碼可讀性
Test Driven Development: By Example
測試 是 TDD 的關鍵
建立 Setup
運行 Exercise
驗證 Verify
拆卸 Teardown
單元測試四階段
自動化單元測試框架
測試框架
測試案例 #1 測試案例#2 ...
產品代碼 ( 測代碼)
report 測試結果 #1
測試結果 #2
...
proj/ src/
objs/ func.o
fun.c
projTest/ src/ funTest.c
objs/ func.o
funcTest.o
目標環境
測試環境
TDD 的好處
加 產生 bug 更少
加 除錯時間更短
加 不會說謊的文件
加 改善設計
加 監督進度
加 心平靜
嵌入式測試驅動開發 有什麼特別的地方嗎?
依賴硬體,浪費時間
雙目標開發 - 解開硬體的依賴
Code
單元測試 運行系統
開發環境 目標環境
嵌入式 TDD 循環
Test-Driven Development for Embedded C
階段一 階段二 階段三 階段四 階段五
開發環境 TDD
交叉 譯 相容測試
評估版 單元測試
目標硬體 單元測試
目標硬體 驗收測試
很頻繁 不頻繁
嵌入式驅動開發工具
加 Unity – C 語言自動化測試框架
– 用 Ruby Script 安裝測試
加 CppUTest – C/C++自動化測試框架
– 用 Ruby Script 測試轉換 Unity 測試
Unity http://throwtheswitch.org/, CppUTest http://cpputest.github.io/
範例 - 開發 LED 驅動程式
玩意 10行程式就搞定了
麼簡單還需要測試嗎?
MyLedDriver.c
#define LED_REGISTER 0x80001234 void LedDriver_Set (uint16_t value) { *((uint16_t *)LED_REGISTER) = value; } uint16_t LedDriver_Get (void) { return *((uint16_t *)LED_REGISTER); }
LedUser.c
void TurnOnLed8 (void) { LedDriver_Set (1 << 8); }
沒有測試把關
再怎麼簡單都可能出錯
TDD 會怎麼做?
測試列表
先寫出測試失敗的測試
LedDriverTest.c
TEST (LedDriver, LedsOffAfterCreate) { uint16_t virtualLeds = 0xffff; LedDriver_Create (&virtualLeds); TEST_ASSERT_EQUAL (0, virtualLeds); }
LedDriver.c
void LedDriver_Create (uint16_t* address) { }
Dependence Injection (依賴注入)
用最簡單的方式讓測試 過
LedDriver.c
void LedDriver_Create (uint16_t* address) { *address = 0; }
$ make compiling LedDriver.c Linking LedDirver_tests Running LedDriver_tests . OK (1 tests, 1 ran, 1 checks, 0 ignored)
再增 一個測試
LedDriverTest.c
TEST (LedDriver, TurnOnLedOne) { uint16_t virtualLeds; LedDriver_Create (&virtualLeds); LedDriver_TurnOn (1); TEST_ASSERT_EQUAL (1, virtualLeds); }
LedDriver.c
void LedDriver_TurnOn (int ledNumber) { }
寫 hardcode 讓測試 過
LedDriver.c
static uint16_t* ledsAddress; void LedDriver_Create (uint16_t* address) { ledsAddress = address; *ledsAddress = 0; } void LedDriver_TurnOn (int ledNumber) { *ledDriver = 1; }
Data Encapsulation (資料封裝)
等等! 不科學啊... hardcode 的實作有問題
增 非當下測試所需的代碼 會降低捕捉各種 bug 的動力
先仿冒再建 保持小而 注的測試
TDD 像是過河的墊腳石
喔... 就是 TDD 嗎?
真正的系統長的像 樣
依賴像一串肉粽
控制輸入 & 監測輸出
直接輸入 直接輸出
間接輸出 間接輸入
斷開魂結 斷開鎖鍊
測試 替身
何時使用測試替身?
加 獨立於硬體
加 注入 以產生的輸入
加 慢的合作者
加 依賴不穩定的事情
加 代未被實現的服務
加 對於 以配置的事物的依賴
測試替身的替換 術
加 譯時期,透過 Preprocessor 替換
加 連結時期,透過 Object File 替換
加 執行時期,透過 Function Pointer 替換
Test Stub
xUnit Test Patterns: Refactoring Test Code
FakeTimeService.c
static int theMinute; void FakeTimeService_SetMinute (int minute) { theMinute = minute; } void TimeService_GetTime (Time* time) { time->minuteOfDay = theMinute; }
FakeTimeServiceTest.c
TEST (FakeTimeService, Set) { Time time; FakeTimeService_SetMinute (42); TimeService_GetTime (&time); LONGS_EQUAL (42, time.minuteOfDay); }
Test Spy
FakeTimeService.c
static int theMinute; int FakeTimeService_GetMinute (void) { return theMinute; } void TimeService_SetDay (Time* time) { theMinute = time->minuteOfDay; }
FakeTimeServiceTest.c
TEST (FakeTimeService, Get) { Time time; time->minuteOfDay = 42; TimeService_SetTime (&time); LONGS_EQUAL (42, FakeTimeService_GetMinute()); }
Mock Object
Flash Program 序列圖 - 的情形
FlashTest.c
TEST (Flash, WriteSucceeds) { int result = 0; MockIO_Expect_Write (0, 0x40); MockIO_Expect_Write (0x1000, 0xBEEF); MockIO_Expect_ReadThenReturn (0, 1<<7); MockIo_Expect_ReadThenReturn (0, 1<<7); MockIO_Expect_ReadThenReturn (0x1000, 0xBEEF); Result = Flash_Write (0x1000, 0xBEEF); LONG_EQUAL (0, result); MockIO_Verify_Complete(); }
CMock http://throwtheswitch.org/, CppUMock http://cpputest.github.io/
怎樣才算好設計? 可測性 可讀性 可維護性
借用物件導向的設計原則
用 C 語言實現資料封裝
WallClock.h
void WallClock_SetTime (Time time); Time WallClock_GetTime (void);
Watch.h
typedef struct WatchStruct Watch; void SetTime (Watch* watch, Time time); Time GetTime (Watch* watch);
interface
Watch
Digital Watch
Watch.h
typedef struct WatchStruct { void (*SetTime) (Watch*, Time); Time (*GetTime) (Watch*); } Watch;
DigitalWatch.c
typedef struct DigitalWatchStruct { Watch* base; Time time; } DigitalWatch; Watch* DigitalWatch_Create (void) { DigitalWatch* self = malloc(sizeof(DigitalWatch)); self->base->SetTime = mySetTime; self->base->GetTime = myGetTime; return (Watch*)self; }
用 C 語言實現類別繼
interface
Watch
Digital Watch
Mechanic Watch
DigitalWatch.c
static void mySetTime (Watch* watch, Time time) { DigitalWatch* self = (DigitalWatch*)watch; self->time = time; }
MechanicWatch.c
static void mySetTime (Watch* watch, Time time) { MechanicWatch* self = (MechanicWatch*)watch; self->time = time; }
用 C 語言實現類別多型 User.c
void doSetTime (Watch* watch, Time time) { watch->SetTime (watch, time); }
User
interface
Watch
Digital Watch
Mechanic Watch
User
SOLID 設計原則
SRP 單一職責
OCP 開放封閉
LSP 替換原則
ISP 介面分
DIP 依賴 轉
Agile Software Development, Principle, Patterns, and Practices
Pocket Watch
隨著代碼不斷增
系統架構變得越來越混亂
代碼的壞味道
加 重複代碼 加 壞名字 加 義大利麵式代碼 加 長函式 加 眼花撩亂的布林運算 加 重複的 switch/case 加 邪惡的嵌套 加 依戀情結 加 參數太多 加 注釋 注釋掉的代碼 加 條件 譯
CodeSmells.c
void foobar (Time* time, Work* work) { if (work->item != NULL) { Day day = time->dayOfWeek; int min = time->minuteOfDay; if ((day >= MONDAY && day <= FRIDAY) && ((min >= 9*60 && min <= 12*60) || (min >= 13*60 && min <= 18*60)) { if (work->type == CODING) doCode (work->item); else doDebug (work->item); // doSomethingImportant (); } } }
重構是 在不改變當前外部行為的條件下, 對現有代碼進行修改的過程
Refactoring: Improving the Design of Existing Code
RefactoredCode.c
static bool isWorkTime (Time* time) { ... } static void workHard (Work* work) { ... } void workInOffice (Time* time, Work* work) { if (!isWorkTime(time)) return; workHard (work); }
唉呦 不錯 重構 個屌
但實際動手 你肯定會講一句話...
砍掉重練比較快
在真實世界裡 必須要跟遺留代碼戰鬥
遺留代碼 = 沒有測試的代碼
修改遺留代碼
• 發現改動點
• 到測試點
• 斷開依賴
• 寫測試
• 改動和重構
Working Efficiently with Legacy Code
測試點
加 接縫 (函式呼 )
加 域變數/感知變量
加 除錯輸出
加 嵌入監控
把遺留代碼放到測試框架中
TestLegacyCode.c
void addNewLegacyCTest() { makeItCompile(); makeItLink(); while (runCrashes()) { findRuntimeDependency(); fixRuntimeDependency(); } addMoreLegacyCTests(); }
TDD 聽來不錯,但是...
不知怎麼開始?
先來場 Coding Dojo 吧
參考資料
Test-Driven Development for Embedded C Test Driven Development: By Example xUnit Test Patterns: Refactoring Test Code Refactoring: Improving the Design of Existing Code Working Efficiently with Legacy Code Agile Software Development, Principle, Patterns, and Practices Design Patterns for Embedded Systems in C: An Embedded Software Engineering Toolkit