Rhion.Mocks今天发布了最新版3.0.1,这是一个Mocking Framework,开发者是Ayende。
面向接口编程的测试难的问题
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>();
只不过是个空壳:)