單元測試如何保證了易用的API

備註:
文章來源於個人這個答案: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()的輸出進行控制. 由於須要作以下改造:安全

API適配

數據庫的對象不經過靜態成員函數獲取,而修改爲注入的方式,這樣方便構造輸入.
數據庫單例類沒有抽象基類,沒法用構造類(或者說是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.單元測試

相關文章
相關標籤/搜索