如何更好的實施單元測試的策略

發表于:2012-12-14來源:51CTO作者:李云點擊數: 標簽:單元測試
我在《單元測試實施解惑(一)》中指出,使用象Cmockery這樣的測試框架,將所需測試的模塊通過打樁的方法實施單元測試并不是最有效的方法。在這篇文章中,讓我們一同來探索更好的方法。在繼續探索之前,讓我從傳統單元測試開始引入所主張的方法。

  我在《單元測試實施解惑(一)》中指出,使用象Cmockery這樣的測試框架,將所需測試的模塊通過打樁的方法實施單元測試并不是最有效的方法。在這篇文章中,讓我們一同來探索更好的方法。在繼續探索之前,讓我從傳統單元測試開始引入所主張的方法。

  圖1中所示的分別是某內存池模塊(mpool.c)和雙向鏈表模塊(dll.c)的代碼片斷,現在讓我們聚焦于為內存池模塊的mpool_buffer_alloc函數實施單元測試。由于該函數使用到了雙向鏈表模塊的dll_pop_head函數,因此,我們需要對dll_pop_head函數進行打樁。(注:實際上還得對global_interrupt_disable和global_interrupt_enable兩函數打樁,但為了簡化我們只以dll_pop_head為例)

  mpool.c void* mpool_buffer_alloc (mpool_handle_t _handle) { interrupt_level_t level; mpool_node_t *p_node; level = global_interrupt_disable (); if (is_invalid_handle (_handle)) { global_interrupt_enable (level); return null; } p_node = (mpool_node_t *)dll_pop_head (&_handle->free_buffer_); if (0 == p_node) { _handle->stats_nobuf_ ++; global_interrupt_enable (level); return null; } global_interrupt_enable (level); p_node->in_use_ = true; return (void *)p_node->addr_; } dll.c dll_node_t *dll_pop_head (dll_t *_p_dll) { dll_node_t *p_node = _p_dll->head_; if (p_node != 0) { _p_dll->count_--; _p_dll->head_ = p_node->next_; if (0 == _p_dll->head_) { _p_dll->tail_ = 0; } else { p_node->next_->prev_ = 0; } p_node->next_ = 0; p_node->prev_ = 0; } return p_node; }

  圖1

  為了便于理解,圖2示例了一個簡化了的樁和mpool_buffer_alloc函數的測試用例。請注意,測試用例中的handle實參假設之前通過mpool_init函數所獲得,圖中同樣為了簡化并未列出。

  stub_dll.c dll_node_t *g_p_node; dll_node_t *dll_pop_head (dll_t *_p_dll) { return g_p_node; } test_mpool.c void test_mpool_buffer_alloc () { mpool_node_t mnode; // set up test environment mnode.addr_ = 0x5A5A5A5A; mnode.in_use_ = false; // do test g_p_node = &mnode.node_; UNITEST_EQUALS (mpool_buffer_alloc (handle), 0x5A5A5A5A); g_p_node = 0; UNITEST_EQUALS (mpool_buffer_alloc (handle), 0); }

  圖2

  對于熟悉Cmockery的讀者,圖3所示的樁函數和測試用例或許看起來更有感覺。

  stub_dll.c dll_node_t *dll_pop_head (dll_t *_p_dll) { return (dll_node_t *)mock (); } test_mpool.c void test_mpool_buffer_alloc () { mpool_node_t mnode; // set up test environment mnode.addr_ = 0x5A5A5A5A; mnode.in_use_ = false; // do test will_return (dll_pop_head, &mnode.node_); assert_int_equal (mpool_buffer_alloc (handler), 0x5A5A5A5A); will_return (dll_pop_head, 0); assert_int_equal (mpool_buffer_alloc (handler), 0); }

  圖3

  需要指出的是,通過打樁的方式,既可以完成狀態檢驗(State Verification),也可以完成行為檢驗(Behavior Verification),這完全取決于樁函數的實現(本文的示例是狀態檢驗)。關于狀態檢驗與行為檢驗更為詳細的內容,請參見Martin Fowler的《Mocks aren’t Stubs》。

  對于沒有單元測試經驗的讀者來說,這里的示例會讓你對單元測試有一定的了解。而對于有單元測試經驗的讀者來說,一定會想到采用打樁的方式所帶來的實施困境。第一,樁函數對被替換函數的行為模擬越接近,單元測試的效果就越好,但所花費的成本開銷也越大。極端情況下,會發現樁代碼與樁所替換的代碼在規模上是相當的。在產品的按時交付壓力之下,實施單元測試所造成的軟件規模增大很難讓團隊做到真心擁抱單元測試。第二,當項目規模增大以后,維護單元測試的樁函數并不是一件簡單的事情。項目規模的增大,易造成各個子團隊維護重復的樁代碼。即使整個項目有著很好的規劃,將所有的樁都以庫的形式進行集中維護,但單元測試代碼的編譯、樁代碼與項目代碼的同步維護仍需相當可觀的工作量。要走出這兩大困境,需要我們就單元測試做一點小小的觀念轉變 — 放棄打樁。

  想一想,為什么不將樁與其所替代的項目代碼整合在一起,從而省去打樁呢?此時,單元測試的實施需要用到我在《專業嵌入式軟件開發》一書中所提出的錯誤注入的方法。大體上,錯誤注入的思想與前面圖2中實現單元測試的方法幾乎一樣,但是將樁函數的代碼與所替換的產品代碼進行了合并。圖4是引入錯誤注入概念之后dll_pop_head函數的實現。

  dll.c dll_node_t *dll_pop_head (dll_t *_p_dll) { dll_node_t *p_node = _p_dll->head_; #ifdef UNIT_TESTING { dll_node_t *p_node; error_t ecode = injected_error_get ( INJECTION_POINT_DLL_POP_HEAD, &p_node); if (ecode != 0) { return p_node; } } #endif if (p_node != 0) { _p_dll->count_--; _p_dll->head_ = p_node->next_; if (0 == _p_dll->head_) { _p_dll->tail_ = 0; } else { p_node->next_->prev_ = 0; } p_node->next_ = 0; p_node->prev_ = 0; } return p_node; }

  圖4

  從圖中可以看出,在產品代碼中我們嵌入了一段用于單元測試的代碼,且通過UNIT_TESTING宏對這段代碼的存在與否進行控制。讀者可以認為這段代碼與樁函數中的代碼功能相似,但最終達到的效果卻有很大的不同。

  首先,UNIT_TESTING所控制的這段代碼存在一個錯誤注入點,這個點以INJECTION_ POINT_DLL_POP_HEAD加以標識。從代碼可以看出,該段代碼先調用injected_error_get函數獲取外部所注入的錯誤及數據。當外部沒有錯誤注入時, dll_pop_head函數的功能與真正的產品代碼是沒有任何區別的(全多了一次對injected_error_get函數的調用),這相當于省去了我們在樁函數中編寫dll_pop_head函數返回不為null的代碼。

  單元測試最難的部分是制造異常情形,比如讓dll_pop_head函數返回null就是我們測試mpool_buffer_alloc函數所需人為制造的。圖5示例了新的單元測試程序是如何制造一個錯誤的。

原文轉自:http://www.anti-gravitydesign.com

国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97