面向接口編程的測試難的問題
Mock Framework的用處在於我們可以在不實現具體對象的情況下,即在沒有某個類的實例的情況下對該對象的行為進行模擬。這一特征對於面向接口的編程非常有用。因為接口的調用者可以在沒有接口的具體實現的情況下使用接口,也就是說調用者可以先於接口的實現者行動。也許有人覺得這好像沒什么神奇的,即使沒有mock我也一樣可以使用接口啊,可是我要問:
“在沒有接口實現的情況下,你能對調用接口的代碼進行測試嗎?”
“NullReferenceException”相信很多人都碰到過的吧。由於接口不能定義構造函數,也就無法實例化,導致了調用接口的代碼無法運行,當然也就是無法測試。
Mocking能干什么?
從mock 的字面意思就可以了解一二了,它的主要工作是模擬出一個被模擬對象的實例,其中包括模擬對該實例的調用行為(比如訪問屬性、調用方法之類)、模擬方法或屬性訪問的返回值、模擬方法和索引的參數傳遞等等,可以說基本上對於一個對象實例的使用它都可以模擬出來。這樣一來,我們就可以好像真的有一個我們需要的實例存在一樣,正常地使用它,來完成對調用者代碼的開發和測試。
Mock object和stub object一樣嗎?
當然不一樣!寫過stub測試程序的人應該知道,stub是真是對象的一個模擬,比如調用者需要一個值,那就讓stub輸出一個值,如果調用者需要傳遞一個值給stub,那就在stub中定義一個方法接受該參數。但是這與mock的對象存在本質的區別:
stub雖然說也是模擬,但其本質上對真是對象的一個簡單實現,而無論它有多簡單它都是一種實現,它是真是存在的,它里面包含了我們定義的操作代碼;
反觀mock的對象,它根本是不存在的,哪怕一句的簡單的不能再簡單的代碼都不存在。
在理解其區別之前,需要明白一點,他們都是為了同一個目標而出現的,代替依賴部分,讓原先的“整合測試”簡化為“單元測試”。
mock:使用easymock等包,在程序代碼中向被測試代碼注入“依賴部分”,通過代碼可編程的方式模擬出函數調用返回的結果。
stub:自己寫代碼代替“依賴部分”。它本身就是“依賴部分”的一個簡化實現。
實際上,在能夠使用mock的時候,就不應該選擇使用stub。但是有時候是必須使用stub的,例如在對遺留代碼進行測試時,該部分代碼不支持“注入”,那么只能將“替代”這個過程外移,使用stub完成此任務了。
應用場景
就以我現在正在開發這個網站代碼為例,來說一下如果在測試的使用Mock object.現在有一個需求,我們需要根據給定的搜索關鍵字和搜索范圍來進行項目的搜索,以MVP的方式實現的話我們定義了一個IView接口:
public interface IView_SearchProject
{
void AttachPresenter(Presenter_SearchProject presenterSearchProject);
SearchRange Range { get;}
string SearchKey { get;}
string UrlBase { get;}
void NavigateTo(string searchUrl);
}
以及一個Presenter:
public class Presenter_SearchProject
{
public Presenter_SearchProject(IView_SearchProject viewSearch)
{
view = viewSearch;
range = view.Range;
prjNav = new ProjectSearchNavigator(view.UrlBase);
query = new SearchQuery();
}
public string GetDesUrl()
{
query.WithDescription = range.WithDescription;
query.WithName = range.WithName;
query.WithKey = range.WithKey;
query.SearchKey = view.SearchKey;
query.Ids = range.Ids;
prjNav.Compile(query);
return prjNav.DestUrl;
}
public void Search()
{
view.NavigateTo(GetDesUrl());
}
private IView_SearchProject view;
private SearchRange range;
private ProjectSearchNavigator prjNav;
private SearchQuery query;
}
ProjectSearchNavigator是一個實現頁面跳轉的幫助類,負責根據View(這里是一個aspx的頁面)傳遞的搜索關鍵字SearchKey和querystring構造出搜索頁面的地址。SearchQuery類負責解析Request.QueryString集合,因為其中存儲的key/value對,需要據此構造出所有查詢條件的一個字符串。
Mocking and Testing
Mocking說到底多試為了測試,否則我們沒有必要,因為mocking出來的對象並不能作為的真是的代碼運行。先把測試的代碼貼出來,再進行解釋,希望你不要覺得太多了:)
[TestFixture]
public class Presenter_SearchProject_Test
{
[SetUp]
public void SetUp()
{
mockRepository=new MockRepository();//1
mockView = mockRepository.CreateMock<IView_SearchProject>();//2
}
[Test]
public void GetDestUrl()
{
SearchRange range = new SearchRange(true, true, false, string.Empty);
//3
//
Expect.Call(mockView.Range).Return(range) ;
//UrlBase
Expect.Call(mockView.UrlBase).Return("http://localhost");
//SearchKey
Expect.Call(mockView.SearchKey).Return("searchKey");
//4
mockRepository.ReplayAll();
//5
presenter = new Presenter_SearchProject(mockView);
string destUrlReturned = presenter.GetDesUrl();
string destUrlExpected = "http://localhost/ProjectPage/ProjectControl.aspx?"
+"search=searchKey&name=True&key=True&description=False";
//6
Assert.AreEqual(destUrlExpected,destUrlReturned);
}
IView_SearchProject mockView;
MockRepository mockRepository;
Presenter_SearchProject presenter;
[TearDown]
public void TestCleanup()
{
mockRepository.ReplayAll();
mockRepository.VerifyAll();
}
}
1. Rhion.Mock框架中要使用mock的對象都需要從MockRepository 這個對象中產生,它充當一個對象工廠的角色。
2. 這一步就是創建我們使用的mock的對象了,需要以被mock類的類型作為泛型參數。
3. 這一步的3行代碼是真正mock的部分,它們分別對應着對mockView的三次調用。
Expect.Call(mockView.Range).Return(range) ;
Expect.Call表示我們希望調用mockVIew的那個方法,也包括屬性。
.Return的意思我們打算讓這個模擬對象返回什么樣的值
綜合起來的意思就是:我們希望mockView的調用者在調用MockView的某一個方法(或屬性)時返回一個有return標識的值
4. ReplayAll的調用千萬不要忘掉,它的意思可以理解為,讓之前設定的模擬行為生效,從此之后我們就可以把這個mock的對象當作是一個真是的對象來使用了。我覺得可以把它想像成CLR為我們自動生成了代碼一樣,為我們生成了一個對被mock對象的實現。
5. 這一步是調用者對mock對象的使用。
6. 測試我們關注的對象的行為是否正常
一個需要注意到地方
presenter = new Presenter_SearchProject(mockView);
像這樣的初始化需要注意順序,必須要等到MockView被真正模擬出來之后,也就是ReplayAll調用之后,因為在presenter 內部需要訪問mockView的成員,比如:
range = view.Range;
但是如果你在mock對象調用者初始化的時候沒有訪問mock對象的成員,那么這樣的初始化可以的。因為雖然mock對象的成員還米有mock出來,但是mock對象已經被生成了:
mockView = mockRepository.CreateMock<IView_SearchProject>();
只不過是個空殼:)