前言:單元測試的時候經常出現一個對象依耐另一個你無法控制的對象,所以這個時候你必須去替代成一個你自己可以控制的對象來擺脫依耐。
比喻我們要通過用戶Id判斷用戶是否存在,那么我們這個方法就會依賴數據庫。這樣就成了集成測試,如果大量的測試就會出現速度慢。
由于依賴數據庫,就會配置和數據庫相關的文件。
比喻這個方法錯誤可能是由于傳入的用戶Id為空,也可能傳入的用戶Id不存在,還可能是數據庫連接斷開等,這樣我們就起不到我們單元測試的目的。
一個外部依賴項:指的是系統中的一個對象,被測試的代碼與這個對象發生交互,但是你不能控制這個對象。比喻前端工程師和后臺工程師合作,前端工程師要等待后臺返回的數據來處理,那么后臺就是他的一個外部依賴項。因為他無法控制后臺的代碼
定義:一個存根(stub)是對系統中存在的一個依賴項(或協作者)的可控制的替代物(就是你找一個對象來替換你無法控制的對象)。通過使用存根,你在測試代碼時無需直接處理這個依賴項。(說白了就是一個你自己定義來對象來取代你無法控制的對象)
其實底層我們就應該使用接口,這樣上層代碼依賴的是接口而不是具體的對象,使項目具有更好的擴展性,當然這里做事為了更好的測試。
從底層方法中抽出一個接口
public interface IUser { /// <summary> /// 檢驗用戶是否存在 /// </summary> /// <param name="userId">用戶名</param> /// <returns></returns> bool IsExist(string userId); }
底層訪問數據庫的類
public class User:IUser { public bool IsExist(string userId) { //從數據庫查詢 //如果有返回true } }
待測試的工作單元
public bool IsExistUser(string userId) { var user = new User(); return user.IsExist(userId); }
一個可控制的存根
public class FackUser:IUser { public bool WillBevalid = false; public bool IsExist(string userId) { return WillBevalid; } }
下面開始注入存根了。
顧名思義就是實例化的時候在構造參數的時候把偽對象注入
此時我們就要修改我們上面的類了如下
被測試類
public class UserBll { PRivate readonly IUser _user; public UserBll(IUser user) { this._user = user; } public bool IsExistUser(string userId) { return _user.IsExist(userId); }
}
測試代碼
[Test] public void IsExistUser_ExistUser_ReturnsTrue() { var fackUser = new FackUser {WillBevalid = true}; var user = new UserBll(fackUser);//注入偽對象 bool result = user.IsExistUser("1"); Assert.IsTrue(result); }
關于構造函數注入的總結:使用構造函數注入比較簡單直觀可讀性和理解方面也很不錯。但是也有問題就是當你依賴越來越多的時候,加入構造函數的參數越來越多這樣就會變得難以維護。
使用場景:比喻api的設計就是某些使用者本身就是帶有參數的構造函數那么就可以這么做。
被測試類
public class UserBll { public IUser User { get; set; } public UserBll(IUser user) { User = new User();//默認的情況執行正常對象 } public bool IsExistUser(string userId) { return User.IsExist(userId); }
}
代碼測試
[Test] public void IsGetName_NormalGetName_ReturnsTrue() { var fackUser = new FackUser { WillBevalid = true }; var user = new UserBll { User = fackUser };//屬性注入 bool result = user.IsExistUser("1"); Assert.IsTrue(result); }
關于屬性注入總結:和構造函數注入相似不過更易讀,更易編寫。
什么時候使用屬性注入:想表明哪個被測試類的某個依賴項是可選的,或者測試可以放心使用默認創建的這個依賴項,就可以屬性注入
我們先看工廠類
public class UserFactory { private IUser _user = null; public IUser Create() { if (_user != null) return _user; return new User(); } [Conditional("DEBUG")] public void SetUser(IUser muser) { _user = muser; } }
被測試類
public class UserBll { public bool IsExistUser(string userId) { var userFactory = new UserFactory();
return userFactory.Create().IsExist(userId); }
測試代碼
[Test] public void IsGetName_NormalGetName_ReturnsTrue() { var fackUser = new FackUser { WillBevalid = true }; var userFactory = new UserFactory(); userFactory.SetUser(fackUser);//設置自己要注入的偽對象 bool result = new UserBll().IsExistUser("1"); Assert.IsTrue(result); }
關于偽造方法的總結: 這種方法很簡單,對工廠添加一個你要控制的偽依賴項。對被測試代碼沒什么改變一切還是原樣。
這種方式明顯比前兩種好。相當于加入了一個工廠的緩沖區。在這里可以做一些邏輯上的處理。
使用這種方法的步驟:
在被測試類:
在測試項目中:
在測試代碼中:
偽造一個工廠方法
public class UserBll { public bool IsExistUser(string userId) { var user = UserManager(); return user.IsExist(userId); } protected virtual IUser UserManager() { return new User(); }
創建新類并集成被測試類
public class TestUser : UserBll { public TestUser(IUser user) { _muser = user; } private readonly IUser _muser; protected override IUser UserManager() { return _muser; } }
測試代碼:
[Test] public void IsGetName_NormalGetName_ReturnsTrue() { var fackUser = new FackUser { WillBevalid = true };//存根實例 var testUser = new TestUser(fackUser);//注入偽對象(新派生的類) bool result = testUser.IsExistUser("1"); Assert.IsTrue(result); }
關于抽取和重寫注入的總結:寫更少的接口,代碼更容易替換。我覺得這種方法最好,就是留了一條路,不光對于測試,如果哪天發現這代碼不好了,直接可以在底層新添加一個替換即可,不會影響原來的代碼
什么時候使用:當你調用外部依賴項時候想模擬自己想要的值的時候就特別受用。
先看被測試類
public class UserBll { public bool IsExistUser(string userId) {return UserManager(userId); } protected virtual bool UserManager(string userId) { IUser user = new User(); return user.IsExist(userId); } }
創建新類并集成被測試類
public class TestUser : UserBll { public bool IsSupported; protected bool IsGetUserName(string userId) { return IsSupported; } }
測試類
public void IsGetName_NormalGetName_ReturnsTrue() { var testUser = new TestUser { IsSupported = true }; bool result = testUser.IsExistUser("1"); Assert.IsTrue(result); }
總結:這和上一種方式其實是很像的,只不過這種更徹底。這種方式更加簡單。不在添加很多的構造函數,設置方法或者工廠類。不過確實不符合面向對象中的封裝原則。暴露了用戶不改看到的東西。
各種依賴注入靈活使用。個人覺得后三種都不錯。
新聞熱點
疑難解答