嵌入式測試驅動開發

71
嵌入式測試驅動開發 Hugo 2/13/2014

Upload: hugo-lu

Post on 14-Nov-2014

707 views

Category:

Education


7 download

DESCRIPTION

《 Test-Driven Development for Embedded C 》心得分享。 TDD(測試驅動開發)是任何開發人員應該掌握的編程實踐,開發者依照需求設計單元測試,然後編寫程式滿足測試,在快速密集的回饋循環中逐漸完善功能,並隨時維持良好的軟體品質。這種開發方式對於物件導向語言陣營的朋友來說應該不陌生,但由於開發環境的特性,使用程序語言的嵌入式平台開發者可能壓根沒聽過或者自認今生無緣。 希望這次交流能為嵌入式平台開發者介紹一些不同於以往的開發方式,打開每個通往敏捷軟體開發的可能。分享內容包含嵌入式TDD原理與策略,單元測試相關工具,如何斷開模組依賴關係,如何得到可測試的設計,以及實務上的建議。

TRANSCRIPT

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

嵌入式測試驅動開發

Hugo

2/13/2014

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

熟悉的開發方式

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

先寫程式,再寫測試 bug

Test-After Development (TAD)

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

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

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

bug 小時候放著不管

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

長大很恐怖

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

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

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

先寫測試 bug,再寫程式

Test-Driven Development (TDD)

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

Test Driven Development 測試驅動開發

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

TDD 循環

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

加 綠燈:快 修改, 做能讓測試 過的工作

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

Test Driven Development: By Example

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

測試 是 TDD 的關鍵

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

建立 Setup

運行 Exercise

驗證 Verify

拆卸 Teardown

單元測試四階段

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

自動化單元測試框架

測試框架

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

產品代碼 ( 測代碼)

report 測試結果 #1

測試結果 #2

...

proj/ src/

objs/ func.o

fun.c

projTest/ src/ funTest.c

objs/ func.o

funcTest.o

目標環境

測試環境

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

TDD 的好處

加 產生 bug 更少

加 除錯時間更短

加 不會說謊的文件

加 改善設計

加 監督進度

加 心平靜

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

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

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

依賴硬體,浪費時間

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

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

Code

單元測試 運行系統

開發環境 目標環境

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

嵌入式 TDD 循環

Test-Driven Development for Embedded C

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

開發環境 TDD

交叉 譯 相容測試

評估版 單元測試

目標硬體 單元測試

目標硬體 驗收測試

很頻繁 不頻繁

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

嵌入式驅動開發工具

加 Unity – C 語言自動化測試框架

– 用 Ruby Script 安裝測試

加 CppUTest – C/C++自動化測試框架

– 用 Ruby Script 測試轉換 Unity 測試

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

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

範例 - 開發 LED 驅動程式

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

玩意 10行程式就搞定了

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

麼簡單還需要測試嗎?

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

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

沒有測試把關

再怎麼簡單都可能出錯

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

TDD 會怎麼做?

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

測試列表

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

先寫出測試失敗的測試

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 (依賴注入)

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

用最簡單的方式讓測試 過

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)

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

再增 一個測試

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) { }

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

寫 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 (資料封裝)

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

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

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

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

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

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

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

TDD 像是過河的墊腳石

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

喔... 就是 TDD 嗎?

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

真正的系統長的像 樣

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

依賴像一串肉粽

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

控制輸入 & 監測輸出

直接輸入 直接輸出

間接輸出 間接輸入

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

斷開魂結 斷開鎖鍊

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

測試 替身

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

何時使用測試替身?

加 獨立於硬體

加 注入 以產生的輸入

加 慢的合作者

加 依賴不穩定的事情

加 代未被實現的服務

加 對於 以配置的事物的依賴

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

測試替身的替換 術

加 譯時期,透過 Preprocessor 替換

加 連結時期,透過 Object File 替換

加 執行時期,透過 Function Pointer 替換

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

Test Stub

xUnit Test Patterns: Refactoring Test Code

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

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

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

Test Spy

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

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

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

Mock Object

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

Flash Program 序列圖 - 的情形

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

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/

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

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

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

借用物件導向的設計原則

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

用 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);

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

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

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

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

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

interface

Watch

Digital Watch

Mechanic Watch

User

SOLID 設計原則

SRP 單一職責

OCP 開放封閉

LSP 替換原則

ISP 介面分

DIP 依賴 轉

Agile Software Development, Principle, Patterns, and Practices

Pocket Watch

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

隨著代碼不斷增

系統架構變得越來越混亂

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

代碼的壞味道

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

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

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

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

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

Refactoring: Improving the Design of Existing Code

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

RefactoredCode.c

static bool isWorkTime (Time* time) { ... } static void workHard (Work* work) { ... } void workInOffice (Time* time, Work* work) { if (!isWorkTime(time)) return; workHard (work); }

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

唉呦 不錯 重構 個屌

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

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

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

砍掉重練比較快

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

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

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

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

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

修改遺留代碼

• 發現改動點

• 到測試點

• 斷開依賴

• 寫測試

• 改動和重構

Working Efficiently with Legacy Code

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

測試點

加 接縫 (函式呼 )

加 域變數/感知變量

加 除錯輸出

加 嵌入監控

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

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

TestLegacyCode.c

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

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

TDD 聽來不錯,但是...

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

不知怎麼開始?

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

先來場 Coding Dojo 吧

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

參考資料

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