2014/02: 嵌入式測試驅動開發

71
嵌嵌嵌嵌嵌嵌嵌嵌嵌 Hugo 2/13/2014

description

 

Transcript of 2014/02: 嵌入式測試驅動開發

Page 1: 2014/02: 嵌入式測試驅動開發

嵌入式測試驅動開發Hugo

2/13/2014

Page 2: 2014/02: 嵌入式測試驅動開發

我們熟悉的開發方式

Page 3: 2014/02: 嵌入式測試驅動開發

先寫程式,再寫測試找 bug

Test-After Development (TAD)

Page 4: 2014/02: 嵌入式測試驅動開發

問題是 ...沒出錯不知道哪裡有 bug

Page 5: 2014/02: 嵌入式測試驅動開發

bug 小時候放著不管

Page 6: 2014/02: 嵌入式測試驅動開發

長大很恐怖

Page 7: 2014/02: 嵌入式測試驅動開發

有什麼方法 ...能盡早把 bug 找出來 ?

Page 8: 2014/02: 嵌入式測試驅動開發

先寫測試找 bug ,再寫程式

Test-Driven Development (TDD)

Page 9: 2014/02: 嵌入式測試驅動開發

TestDrivenDevelopment測試驅動開發

Page 10: 2014/02: 嵌入式測試驅動開發

TDD 循環

• 紅燈:增加一個運行失敗,甚至無法編譯的測試。

• 綠燈:快速修改,只做能讓測試通過的工作。

• 黃燈:重構,移除重複改進代碼可讀性。

《 Test Driven Development: By Example》

Page 11: 2014/02: 嵌入式測試驅動開發

測試 是 TDD 的關鍵

Page 12: 2014/02: 嵌入式測試驅動開發

建立Setup建立

Setup運行

Exercise運行

Exercise驗證

Verify驗證

Verify拆卸

Teardown拆卸

Teardown

單元測試四階段

Page 13: 2014/02: 嵌入式測試驅動開發

自動化單元測試框架

測試框架測試框架

測試案例 #1測試案例 #1 測試案例 #2測試案例 #2 ...

產品代碼 ( 受測代碼 )產品代碼 ( 受測代碼 )

report 測試結果 #1測試結果 #1

測試結果 #2測試結果 #2

...

proj/ src/objs/ func.o

fun.c

projTest/ src/ funTest.cobjs/ func.o

funcTest.o

目標環境

測試環境

Page 14: 2014/02: 嵌入式測試驅動開發

TDD 的好處• 產生 bug 更少• 除錯時間更短• 不會說謊的文件• 改善設計• 監督進度• 內心平靜

Page 15: 2014/02: 嵌入式測試驅動開發

嵌入式測試驅動開發有什麼特別的地方嗎 ?

Page 16: 2014/02: 嵌入式測試驅動開發

依賴硬體,浪費時間

Page 17: 2014/02: 嵌入式測試驅動開發

雙目標開發 - 解開硬體的依賴

CodeCode

單元測試單元測試 運行系統運行系統

開發環境開發環境 目標環境目標環境

Page 18: 2014/02: 嵌入式測試驅動開發

嵌入式 TDD 循環

《 Test-Driven Development for Embedded C》

階段一 階段二 階段三 階段四 階段五

開發環境TDD

交叉編譯相容測試

評估版單元測試

目標硬體單元測試

目標硬體驗收測試

很頻繁 不頻繁

Page 19: 2014/02: 嵌入式測試驅動開發

嵌入式驅動開發工具

• Unity– C 語言自動化測試框架– 用 Ruby Script 安裝測

試• CppUTest

– C/C++ 自動化測試框架– 用 Ruby Script 將測試

轉換成 Unity 測試

Unity http://throwtheswitch.org/, CppUTest http://cpputest.github.io/

Page 20: 2014/02: 嵌入式測試驅動開發

範例 - 開發 LED 驅動程式

Page 21: 2014/02: 嵌入式測試驅動開發

這玩意我 10 行程式就搞定了

Page 22: 2014/02: 嵌入式測試驅動開發

這麼簡單還需要測試嗎 ?MyLedDriver.cMyLedDriver.c

#define LED_REGISTER 0x80001234void LedDriver_Set (uint16_t value){ *((uint16_t *)LED_REGISTER) = value;}uint16_t LedDriver_Get (void){ return *((uint16_t *)LED_REGISTER);}

#define LED_REGISTER 0x80001234void LedDriver_Set (uint16_t value){ *((uint16_t *)LED_REGISTER) = value;}uint16_t LedDriver_Get (void){ return *((uint16_t *)LED_REGISTER);}

LedUser.cLedUser.c

void TurnOnLed8 (void){ LedDriver_Set (1 << 8);}

void TurnOnLed8 (void){ LedDriver_Set (1 << 8);}

Page 23: 2014/02: 嵌入式測試驅動開發

沒有測試把關再怎麼簡單都可能出錯

Page 24: 2014/02: 嵌入式測試驅動開發

TDD 會怎麼做 ?

Page 25: 2014/02: 嵌入式測試驅動開發

測試列表

Page 26: 2014/02: 嵌入式測試驅動開發

先寫出測試失敗的測試LedDriverTest.cLedDriverTest.c

TEST (LedDriver, LedsOffAfterCreate){ uint16_t virtualLeds = 0xffff; LedDriver_Create (&virtualLeds); TEST_ASSERT_EQUAL (0, virtualLeds);}

TEST (LedDriver, LedsOffAfterCreate){ uint16_t virtualLeds = 0xffff; LedDriver_Create (&virtualLeds); TEST_ASSERT_EQUAL (0, virtualLeds);}

LedDriver.cLedDriver.c

void LedDriver_Create (uint16_t* address){}

void LedDriver_Create (uint16_t* address){}

Dependence Injection ( 依賴注入 )

Page 27: 2014/02: 嵌入式測試驅動開發

用最簡單的方式讓測試通過LedDriver.cLedDriver.c

void LedDriver_Create (uint16_t* address){ *address = 0;}

void LedDriver_Create (uint16_t* address){ *address = 0;}

$ makecompiling LedDriver.cLinking LedDirver_testsRunning LedDriver_tests.OK (1 tests, 1 ran, 1 checks, 0 ignored)

Page 28: 2014/02: 嵌入式測試驅動開發

再增加一個測試LedDriverTest.cLedDriverTest.c

TEST (LedDriver, TurnOnLedOne){ uint16_t virtualLeds; LedDriver_Create (&virtualLeds); LedDriver_TurnOn (1); TEST_ASSERT_EQUAL (1, virtualLeds);}

TEST (LedDriver, TurnOnLedOne){ uint16_t virtualLeds; LedDriver_Create (&virtualLeds); LedDriver_TurnOn (1); TEST_ASSERT_EQUAL (1, virtualLeds);}

LedDriver.cLedDriver.c

void LedDriver_TurnOn (int ledNumber){}

void LedDriver_TurnOn (int ledNumber){}

Page 29: 2014/02: 嵌入式測試驅動開發

寫 hardcode 讓測試通過LedDriver.cLedDriver.c

static uint16_t* ledsAddress;

void LedDriver_Create (uint16_t* address){ ledsAddress = address; *ledsAddress = 0;}

void LedDriver_TurnOn (int ledNumber){ *ledDriver = 1;}

static uint16_t* ledsAddress;

void LedDriver_Create (uint16_t* address){ ledsAddress = address; *ledsAddress = 0;}

void LedDriver_TurnOn (int ledNumber){ *ledDriver = 1;}

Data Encapsulation ( 資料封裝 )

Page 30: 2014/02: 嵌入式測試驅動開發

等等 ! 這不科學啊 ...hardcode 的實作有問題

Page 31: 2014/02: 嵌入式測試驅動開發

增加非當下測試所需的代碼會降低捕捉各種 bug 的動力

Page 32: 2014/02: 嵌入式測試驅動開發

先仿冒再建造保持小而專注的測試

Page 33: 2014/02: 嵌入式測試驅動開發

TDD 像是過河的墊腳石

Page 34: 2014/02: 嵌入式測試驅動開發

喔 ... 這就是 TDD 嗎 ?

Page 35: 2014/02: 嵌入式測試驅動開發

真正的系統長的像這樣

Page 36: 2014/02: 嵌入式測試驅動開發

依賴像一串肉粽

Page 37: 2014/02: 嵌入式測試驅動開發

控制輸入 & 監測輸出

直接輸入 直接輸出

間接輸入 間接輸出

Page 38: 2014/02: 嵌入式測試驅動開發

斷開魂結、斷開鎖鍊

Page 39: 2014/02: 嵌入式測試驅動開發

測試 替身

Page 40: 2014/02: 嵌入式測試驅動開發

何時使用測試替身 ?•獨立於硬體• 注入難以產生的輸入• 加速緩慢的合作者• 依賴不穩定的事情•取代未被實現的服務•對於難以配置的事物的依賴

Page 41: 2014/02: 嵌入式測試驅動開發

測試替身的替換技術• 編譯時期,透過 Preprocessor 替換•連結時期,透過 Object File 替換•執行時期,透過 Function Pointer 替換

Page 42: 2014/02: 嵌入式測試驅動開發

Test Stub

《 xUnit Test Patterns: Refactoring Test Code》

Page 43: 2014/02: 嵌入式測試驅動開發

FakeTimeService.cFakeTimeService.c

static int theMinute;

void FakeTimeService_SetMinute (int minute){ theMinute = minute;}void TimeService_GetTime (Time* time){ time->minuteOfDay = theMinute;}

static int theMinute;

void FakeTimeService_SetMinute (int minute){ theMinute = minute;}void TimeService_GetTime (Time* time){ time->minuteOfDay = theMinute;}

FakeTimeServiceTest.cFakeTimeServiceTest.c

TEST (FakeTimeService, Set){ Time time;

FakeTimeService_SetMinute (42); TimeService_GetTime (&time); LONGS_EQUAL (42, time.minuteOfDay);}

TEST (FakeTimeService, Set){ Time time;

FakeTimeService_SetMinute (42); TimeService_GetTime (&time); LONGS_EQUAL (42, time.minuteOfDay);}

Page 44: 2014/02: 嵌入式測試驅動開發

Test Spy

Page 45: 2014/02: 嵌入式測試驅動開發

FakeTimeService.cFakeTimeService.c

static int theMinute;

int FakeTimeService_GetMinute (void){ return theMinute;}void TimeService_SetDay (Time* time){ theMinute = time->minuteOfDay;}

static int theMinute;

int FakeTimeService_GetMinute (void){ return theMinute;}void TimeService_SetDay (Time* time){ theMinute = time->minuteOfDay;}

FakeTimeServiceTest.cFakeTimeServiceTest.c

TEST (FakeTimeService, Get){ Time time;

time->minuteOfDay = 42; TimeService_SetTime (&time); LONGS_EQUAL (42, FakeTimeService_GetMinute());}

TEST (FakeTimeService, Get){ Time time;

time->minuteOfDay = 42; TimeService_SetTime (&time); LONGS_EQUAL (42, FakeTimeService_GetMinute());}

Page 46: 2014/02: 嵌入式測試驅動開發

Mock Object

Page 47: 2014/02: 嵌入式測試驅動開發

Flash Program 序列圖 - 成功的情形

Page 48: 2014/02: 嵌入式測試驅動開發

FlashTest.cFlashTest.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();}

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/

Page 49: 2014/02: 嵌入式測試驅動開發

怎樣才算好設計 ?可測性、可讀性、可維護性

Page 50: 2014/02: 嵌入式測試驅動開發

借用物件導向的設計原則

Page 51: 2014/02: 嵌入式測試驅動開發

用 C 語言實現資料封裝

WallClock.hWallClock.h

void WallClock_SetTime (Time time);Time WallClock_GetTime (void);void WallClock_SetTime (Time time);Time WallClock_GetTime (void);

Watch.hWatch.h

typedef struct WatchStruct Watch;void SetTime (Watch* watch, Time time);Time GetTime (Watch* watch);

typedef struct WatchStruct Watch;void SetTime (Watch* watch, Time time);Time GetTime (Watch* watch);

Page 52: 2014/02: 嵌入式測試驅動開發

《 interface》Watch

《 interface》Watch

DigitalWatch

DigitalWatch

Watch.hWatch.h

typedef struct WatchStruct { void (*SetTime) (Watch*, Time); Time (*GetTime) (Watch*);} Watch;

typedef struct WatchStruct { void (*SetTime) (Watch*, Time); Time (*GetTime) (Watch*);} Watch;

DigitalWatch.cDigitalWatch.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;}

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 語言實現類別繼承

Page 53: 2014/02: 嵌入式測試驅動開發

《 interface》Watch

《 interface》Watch

DigitalWatch

DigitalWatch

MechanicWatch

MechanicWatch

DigitalWatch.cDigitalWatch.c

static void mySetTime (Watch* watch, Time time){ DigitalWatch* self = (DigitalWatch*)watch; self->time = time;}

static void mySetTime (Watch* watch, Time time){ DigitalWatch* self = (DigitalWatch*)watch; self->time = time;}

MechanicWatch.cMechanicWatch.c

static void mySetTime (Watch* watch, Time time){ MechanicWatch* self = (MechanicWatch*)watch; self->time = time;}

static void mySetTime (Watch* watch, Time time){ MechanicWatch* self = (MechanicWatch*)watch; self->time = time;}

用 C 語言實現類別多型User.cUser.c

void doSetTime (Watch* watch, Time time) { watch->SetTime (time);}

void doSetTime (Watch* watch, Time time) { watch->SetTime (time);}

UserUser

Page 54: 2014/02: 嵌入式測試驅動開發

《 interface》Watch

《 interface》Watch

DigitalWatch

DigitalWatch

MechanicWatch

MechanicWatch

UserUser

SOLID 設計原則SRP 單一職責OCP 開放封閉LSP 替換原則ISP 介面分離DIP 依賴倒轉

《 Agile Software Development, Principle, Patterns, and Practices》

PocketWatch

PocketWatch

Page 55: 2014/02: 嵌入式測試驅動開發

隨著代碼不斷增加系統架構變得越來越混亂

Page 56: 2014/02: 嵌入式測試驅動開發

代碼的壞味道• 重複代碼• 壞名字• 義大利麵式代碼• 長函式• 眼花撩亂的布林運算• 重複的 switch/case• 邪惡的嵌套• 依戀情結• 參數太多• 注釋、注釋掉的代碼• 條件編譯

Page 57: 2014/02: 嵌入式測試驅動開發

CodeSmells.cCodeSmells.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 (); } }}

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 (); } }}

Page 58: 2014/02: 嵌入式測試驅動開發

重構是“在不改變當前外部行為的條件下,對現有代碼進行修改的過程。”

《 Refactoring: Improving the Design of Existing Code》

Page 59: 2014/02: 嵌入式測試驅動開發

RefactoredCode.cRefactoredCode.c

static bool isWorkTime (Time* time){ ...}static void workHard (Work* work){ ...}

void workInOffice (Time* time, Work* work){ if (!isWorkTime(time)) return;

workHard (work);}

static bool isWorkTime (Time* time){ ...}static void workHard (Work* work){ ...}

void workInOffice (Time* time, Work* work){ if (!isWorkTime(time)) return;

workHard (work);}

Page 60: 2014/02: 嵌入式測試驅動開發

唉呦 不錯 重構這個屌

Page 61: 2014/02: 嵌入式測試驅動開發

但實際動手你肯定會講一句話 ...

Page 62: 2014/02: 嵌入式測試驅動開發

砍掉重練比較快

Page 63: 2014/02: 嵌入式測試驅動開發

在真實世界裡我們必須要跟遺留代碼戰鬥

Page 64: 2014/02: 嵌入式測試驅動開發

遺留代碼 = 沒有測試的代碼

Page 65: 2014/02: 嵌入式測試驅動開發

修改遺留代碼• 發現改動點• 找到測試點• 斷開依賴• 寫測試• 改動和重構

《Working Efficiently with Legacy Code》

Page 66: 2014/02: 嵌入式測試驅動開發

測試點•接縫 (函式呼叫 )•全域變數 /感知變量

• 除錯輸出• 嵌入監控

Page 67: 2014/02: 嵌入式測試驅動開發

把遺留代碼放到測試框架中

TestLegacyCode.cTestLegacyCode.c

void addNewLegacyCTest(){ makeItCompile(); makeItLink(); while (runCrashes()) { findRuntimeDependency(); fixRuntimeDependency(); } addMoreLegacyCTests();}

void addNewLegacyCTest(){ makeItCompile(); makeItLink(); while (runCrashes()) { findRuntimeDependency(); fixRuntimeDependency(); } addMoreLegacyCTests();}

Page 68: 2014/02: 嵌入式測試驅動開發

TDD 聽來不錯,但是 ...

Page 69: 2014/02: 嵌入式測試驅動開發

不知怎麼開始 ?

Page 70: 2014/02: 嵌入式測試驅動開發

先來場 Coding Dojo 吧

Page 71: 2014/02: 嵌入式測試驅動開發

參考資料《 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 Soft

ware Engineering Toolkit 》