在教程开始之前,先回顾下单元测试应该有哪些特点?
运行快,自动化,结果稳定,隔离等等。
但是并不是所有的方法都能写出这样的单元测试。比如说有如下类:
c#
public class Foo
{
private Log _log;
public Foo(Log log)
{
_log = log;
}
public void DoA()
{
//do something
_log.Write("Finish A");
}
}这个类依赖于一个日志对象Log
c#
public class Log
{
private readonly string _uri;
public Log(string uri)
{
_uri = uri;
}
public void Write(string text)
{
using (var stream = File.AppendText(_uri))
{
stream.Write(text);
}
}
public string Read()
{
if (!File.Exists(_uri))
{
return string.Empty;
}
using (var stream = File.OpenRead(_uri))
{
using (var reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}
}
}现在我们期望能够写一个单元测试,验证运行DoA方法时,是否向日志写入了Finish A
那么问题来了,我们需要在每次运行单元测试时,要真正的读写文件。那么这个单元测试能够做到运行快,结果稳定,隔离等等要求么?如果我们的例子中的日志系统换成数据库,网络请求会怎样呢?
如果这个时候我们能够伪造一个日志系统,是否问题就能够解决了呢?
那么首先我们需要引入一个接口ILog
C#
public interface ILog
{
void Write(string text);
string Read();
}
public class Log:ILog
{
//some code
}相应的,待测类也要做一些改动
c#
public class Foo
{
private ILog _log;
public Foo(ILog log)
{
_log = log;
}
public void DoA()
{
//do something
_log.Write("Finish A");
}
}这样我们就可以制造一个FakeLog将测试环境同真实的文件系统进行隔离。

好,我们看下FakeLog的代码
c#
public class FakeLog : ILog
{
public string Log { get; private set; }
public void Write(string text)
{
Log = text;
}
public string Read()
{
throw new NotImplementedException();
}
}我们要测试的只有日志写入,因此Read方法不需要实现,另外我们还需要一个简单的方式能够把写入的内容暴露出来。所以这里还定义了一个属性Log用于展示写入的日志内容。
下面是测试代码
c#
[TestClass]
public class UnitTest1
{
[TestMethod]
public void TestMethod1()
{
var fakeLog = new FakeLog();
var foo = new Foo(fakeLog);
foo.DoA();
Assert.AreEqual("Finish A",fakeLog.Log);
}
}运行一下,绿色,很好。

那么就结束了么?不是的,单元测试还有一个特点是易于编写。我们这样子手工编写一个单元测试成本还是太高了。这么简单的东西是不是应该有一个隔离框架来做呢?
于是就到了我们的主角,Moq。
先让大家体验一下使用Moq的自动创建代码来替换我们手动创建的FakeLog。
c#
[TestMethod]
public void TestMethod2()
{
var fakeLog = new Mock<ILog>();
var foo = new Foo(fakeLog.Object);
foo.DoA();
fakeLog.Verify(log => log.Write("Finish A"));
}就是这么简单,完全不用自己实现FakeLog,并且用一个lambda表达式就完成了我们期望的验证。
运行测试,绿色,通过。

很棒是不是,这就是自动化的隔离框架的作用。
接下去就会带你进入Moq的世界