備註:
文章來源於個人這個答案:TDD的意義. 由於原題目被鎖了,因此搬移出來.
本文不強烈區分TDD
和單元測試
, 雖然前者是方法論, 後者是實踐這一方法論的具體技術. 不過大多數場景都是用單元測試來踐行TDD的.c++
通常而言TDD的好處是以輸出爲導向及早發現問題,以及方便重構(單元測試保證).
我理解,還有一個比較重要的意義是: 客觀上強制了程序員寫出更加友好的接口 方便測試和聯調.程序員
這裏我以c++舉例,需求就用最簡單的: 實現一個單例類(好比說一個讀取數據庫的單例).
好,拿到這個需求了,考慮到c++11以後static自己就是多線程安全的,因此實現一個單例模式就很簡單了,以下:數據庫
// 手打不保證編譯ok class SingleDb { public: int getMoney(){...} SinlgeDb(const SingleDb &) = delete; void operator=(const SingleDb &) = delete; static SingleDb &get() { static SingleDb db; return db; } };
ok, 單例類的主體工做基本上就完成了,代碼中直接能夠用SingleDb::get()就能夠得到這個單例,再補充和業務相關的讀取等成員函數便可.
好了,原本這樣就ok了,可是老闆如今要求你們每一個新功能都要求寫單元測試,客戶端程序員(這裏的客戶端指代的是使用這個單例模式的程序員)調用了這個API以後就不爽了,由於他想要對他本身的業務代碼進行單元測試,可是在讀數據庫的時候沒法進行打樁測試。具體遇到的問題以下:segmentfault
struct Client{ int doSth() const { SingleDb &db = SingleDb::get(); if(db.getMoney() < 0) return -1; } };
徹底沒法測試,由於咱們在寫單元測試的時候沒法控制db.getMoney()的輸出進行控制. 由於須要作以下改造:安全
數據庫的對象不經過靜態成員函數獲取,而修改爲注入的方式,這樣方便構造輸入.
數據庫單例類沒有抽象基類,沒法用構造類(或者說是Mock對象)替換, 需改改寫成有繼承體系的類.
實現:
針對以上兩點,修改以後的樣子多線程
struct DbBase { virtual int getMoney() = 0; }; class SingleDb: public DbBase { // 沒啥變化 }; //客戶端 struct Client { DbBase &db; explicit Client(DbBase &db): db(db) {} int doSth() const { if(db.getMoney() < 0) return -1; } };
這樣處理以後,單例類已經變成了一個更加易於單元測試的類了,以一個比較簡單的單元測試框架做爲例子給出(catch+fakeit)框架
TEST_CASE("", "") { using namespace fakeit; Mock mock; when(Method(mock, getMoney())).Return(-1); Client c(mock.get()); REQUIRE(c.doSth() == -1); }
這其實是爲了單元測試而把API的接口改了,不單單是更加易於單元測試了. 更有意義的是,把接口提高至抽象類,之後想擴展實現類,直接就新增繼承類便可,單元測試都不用動(由於傳入的是抽象基類).
或者哪一天用到的單元測試框架沒人維護了須要切換單元測試代碼,那業務代碼根本也不須要動,由於抽象接口已經固定. 不用Mock框架,本身打個樁都能單元測試, 例子以下:函數
// 構造打樁繼承類 struct DataBaseMock : public DbBase int getMoney() {return -1;} }; // 測試 ... DataBaseMock db; Client c(&db); REQUIRE(c.doSth() == -1);
實際上,撰寫一個好的API的好處自己又是另一個話題了(不單單有助於單元測試),可是TDD這個開發模式可以強迫程序員寫出一個更加易用的API.單元測試