Interface在C#中的使用频率是非常高的,为了满足面向对象的需求,接口声明了对象所具有的行为,利用其可以实现具有可扩展性的类,例如可以通过返回接口,返回具体的类,而隐藏类的实现细节,并且仅仅暴露该接口所支持的属性和方法。又或者可以通过接口传递参数,利用多态实现系统的可扩展性。这里,我想演示的是,使用接口创建Mock对象。
首先解释下什么是Mock对象,这就好比拍电影的时候的替身演员,比如要完成一些高难度的动作,需要具有特殊技能的人代替真正的演员。Mock对象就是这个意思。在实际单元测试中,所需要的对象可能创建代价特别高或者不稳定,比如数据库连接,或者网络连接,这样一些不确定的因素在测试中会影响测试结果,在做团队开发的时候我们常常也可以听到这样的抱怨:“把服务器打开,我这边要测试..。”实际上测试的并不是底层连接,而是返回的数据是否能在被测的方法中正常运行。呵呵,或许您又有办法了,在方法里面注释掉从数据库读数据的部分,给变量手动赋值,Well good idea. 那么面对成千上万的代码,你完成了测试你还得把测试代码擦干净。 Oh, 下次测试怎么办?再加?#%#◎◎×。另外一点,有时候你想测的代码引用了还没开发出来的对象,仅仅是用它的接口。由此,你更需要用Mock来测试了。
下面说说怎么做:
1.我们可以按照这个原理自己手写Mock对象,但是我们也可以使用工具,这里使用一个开源Mock框架DotNetMock(http://sourceforge.net/projects/dotnetmock)。
2. 首先,你应该估计出Mock对象是什么样子了。我们可以手动的写写这个对象。
假设有个类,它提供记录日志的功能,如下:
public class DataAccessor
{
ILogger _logger;
public DataAccessor(ILogger logger)
{
_logger = logger;
}
public void LogData()
{
_logger.SetName("DataAccessor");
_logger.Log("new log1");
_logger.Log("new log2");
}
}
我们创建了ILogger确定接口方法。并作为构造函数参数传入。ILogger接口如下
public class Logger : ILogger
{
#region ILogger Members
public void SetName(string name)
{
return;
}
public void Log(string logVal)
{
return;
}
#endregion
}
接着,写我们的MockLogger,我们把它放置在我们的测试类中,并放在一个不同的文件里,千万不要和产品代码放在一起从而导致客户的抱怨。然后,为了使用这个开源框架,我们添加引用DotNetMock, MockLogger不仅继承于ILogger,还继承于MockObject:
public class MockLogger : MockObject, ILogger
{
private ExpectationValue _name = new ExpectationValue("name");
private ExpectationArrayList _msgs = new ExpectationArrayList("msgs");
public string ExpectedName
{
set { _name.Expected = value; }
}
public void AddExpectedMsg(string msg)
{
_msgs.AddExpected(msg);
}
#region ILogger Members
public void SetName(string name)
{
_name.Actual = name;
}
public void Log(string logVal)
{
_msgs.AddActual(logVal);
}
#endregion
}
按照这里的需要,我们用DotNetMock 提供的ExpectationValue和ExpectationArrayList用来存放Logger的名称以及它记录的字符串集合。并添加了两个方法。
最后,我们在Test框架中使用这个测试类,这里用的是NUnit:
[TestFixture, Category("Full")]
public class TestClass
{
[Test]
public void TestLog()
{
MockLogger logger = new MockLogger();
DataAccessor acc = new DataAccessor(logger);
acc.LogData();
logger.ExpectedName = "DataAccessor";
logger.AddExpectedMsg("new log1");
logger.AddExpectedMsg("new log2");
logger.Verify();
}
}
那个Verify()方法帮我们完成了测试。OK, 我们的替身做好了,也测试了。可能真正的Logger还没开发呢,但是我们已经可以测试DataAccessor的方法了。同样的,我们可以创建数据库连接等Mock对象,但是等等,其实我们可以更好的使用这个DotNetMock框架,试试使用DotNetMock.Framework下的Data中已经有的Mock对象吧。
另外,可能你会问那么如果需要Mock的对象有几千个方法,我只用其中几个,那我不疯掉?其实有一种叫做动态Mock的对象是不需要你全部写满的。这里我就不介绍了,有兴趣的自己找资料吧。所有的信息均参照于《单元测试之道-C#版》,上面有更好的讲解,这里看到他的例子,想想接口还能有这样的好处,所以就写了这个文章,呵呵。