基于狀態(tài)測試
??在上一篇文章中,我們舉了一個帶返回值的例子,那么無返回值的情況下又該怎樣寫單元測試呢?
有如下代碼:
public IList<string> Names = new List<string>();
public void Reset()
{
Names.Clear();
}
我們發(fā)現(xiàn),Reset方法內部執(zhí)行的是Names列表的清空操作,這個操作可以抽象成對被測試類狀態(tài)的更改,要驗證狀態(tài)更改是否符合預期,我們只需要驗證更改前后是否符合預期即可。在這里,只需要測試Reset方法是否按照我們預期的把Names清空即可。如下:
/// <summary>
/// 條件:Names不為空
/// 預期:清空Names
/// </summary>
[TestMethod()]
public void ResetTest_NamesNotEmpty_NamesEmpty()
{
//Arrange
var document = new Document();
document.Names.Add("name0");
document.Names.Add("name1");
//Action
document.Reset();
//Assert
Assert.AreEqual(document.Names.Count, 0);
}
依賴外部對象的測試
??單元測試需要能夠快速獨立運行,隔離掉對外部的依賴是非常必要的,比如文件系統(tǒng)、硬件數(shù)據(jù)、web服務等。
如下代碼:
///<summary>
/// 判斷當前字符串是否是合法的html字符串
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public bool IsValidHtml(string input)
{
var textService = new TextService();
return textService.IsValidHtml(input);
}
可以看到,當前方法依賴TextService來驗證html,但是在運行單元測試時,TextService的狀態(tài)是未知的,它甚至可能還未開發(fā)完成。因此,需要隔離掉對TextService的依賴。
而TextService是在IsValidHtml方法內部創(chuàng)建的,我們無法隔離,這個時候就需要對方法進行一系列的修改,以使得它達到可測試的要求(這就是所謂的單元測試約束設計)。
再進一步的分析,可以發(fā)現(xiàn)依賴的是TextService提供的IsValidHtml()方法,而并非TextService這個對象,這就好說了,讓IsValidHtml()依賴可以提供html驗證的接口,我們就可以不用依賴TextService這個對象了,我們抽取接口:
public interface ITextService
{
bool IsValidHtml(string input);
}
這樣我們就可以從對具體實現(xiàn)的依賴解耦為對接口的依賴,因此,在測試方法中我們可以很方便的用一個假的ITextService的實現(xiàn)來替代真實的TextService,由此隔離對真實外部服務的依賴。
這個假的ITextService的實現(xiàn)我們稱為 偽對象。
如下SubTextService就是我們的偽對象:
public class SubTextService : ITextService
{
private bool _isValidHtml;
public void SetIsValidHtml(bool value)
{
_isValidHtml = value;
}
public bool IsValidHtml(string input)
{
return _isValidHtml;
}
}
有了偽對象,怎么使用起來呢?
接下來介紹幾種偽對象注入的方式
-
構造函數(shù)注入
這種方式需要被測試類提供一個帶有ITextService參數(shù)的構造函數(shù),我們修改被測試類:
接下來,在測試方法中就可以將偽對象注入進去了:public Document(ITextService textService) { _textService = textService; } /// <summary> /// 判斷當前字符串是否是合法的html字符串 /// </summary> /// <param name="input"></param> /// <returns></returns> public bool IsValidHtml(string input) { return _textService.IsValidHtml(input); }
這種方法比較簡單,被測試類的代碼改動也不大。/// <summary> /// 條件:傳入Empty的字符串 /// 預期:返回False /// </summary> [TestMethod()] public void IsValidHtml_EmptyInput_ReturnFalse() { //Arrange var subTextService = new SubTextService(); subTextService.SetIsValidHtml(false); var document = new Document(subTextService); //Action var result = document.IsValidHtml(string.Empty); //Assert Assert.IsFalse(result); }
但是,如果方法中依賴多個外部接口,需要構造函數(shù)的參數(shù)列表可能很長;或者被測試類中不同方法依賴了不同的外部接口,那么需要增加多個構造函數(shù)。
因此,此方法需要根據(jù)情況謹慎使用。 -
屬性注入
這種方式指的是被測試類將外部接口的依賴設計成可以公開屬性:
這樣在單元測試中就可以方便的將偽對象注入進去。public ITextService TextService { get; set; }
這種方法簡單,對被測試類改動小。
但是,將TextService設計成屬性,會給外部一種TextService的賦值非必需的誤解,然而在我們的設計中TextService是必須的。
因此,不推薦使用。 -
工廠注入
工廠注入指的是當我們依賴的第三方接口是用工廠新建時,通過給工廠中注入偽對象來隔離對真實對象的依賴。
這種方法也比較簡單,需要對工廠方法進行修改,改動量也不大。public static class TextServiceFactory { private static ITextService _textService = new TextService(); public static ITextService Create() { return _textService; } public static void SetTextService(ITextService textService) { _textService = textService; } }
可根據(jù)情況使用。
-
派生類注入
派生類注入指的是在設計的時候,把對外部的依賴對象的獲取設計成可以被繼承,這樣偽對象就可以在不修改原來代碼的情況下完成注入:
寫單元測試的時候,只需要用偽對象繼承被測試類,就可以在重寫GetTextService時,注入偽對象。protected virtual ITextService GetTextService() { return new TextService(); }
在單元測試時,就直接使用SubDocument即可.//Document為被測試類 public class SubDocument : Document { protected override ITextService GetTextService() { return new SubTextService(); } }
這種方法比較簡單,而且不需要修改被測試類代碼。
推薦此方法。
??寫單元測試可以為我們的代碼增加一層保護,在設計程序時考慮單元測試也可以優(yōu)化我們的設計,好處多多,何樂而不為呢(●'?'●)
2017-3-17 23:29:07