Mock 用法示例
Mock 用法示例
引言
Mock 是一个简单轻量级的隔离框架,它建立在匿名方法和表达式树的基础上。为了创建它们,它使用了代码生成,从而允许模拟接口、虚方法(甚至受保护的方法),但不允许模拟非虚方法和static
方法。
注意
市场上只有两个框架允许模拟一切。它们是TypeMockIsolator
和Microsoft Fakes,可用于Visual Studio 2012及更高版本。这些框架与Mock(使用代码生成)不同,它们使用CLR Profiling API,允许模拟任何方法,包括static
、virtual
和private
方法。恕我直言,它们对于测试难以或不可能一次性重构的遗留代码很有用。
在Mock中,桩(stubs)和模拟(mocks)之间没有区别。或者更正式地说,状态验证和行为验证之间没有区别。尽管区分它们并非总是一件容易的事,但很多时候同一个元素可以扮演两种角色。我们将从简单到复杂的例子进行探讨。起初,我们将考虑状态验证,之后,我们将切换到行为验证。
状态验证
例如,我们将考虑以下接口的一组单元测试
public interface ILoggerSomeDependency
{
string GetApplicationDirectory();
string GetDirectoryForDependencyByLoggerName(string loggerName);
string GetLoggerInstance{get;}
}
- 方法
GetApplicationDirectory
的桩//Mock.Of returns dependency itself proxy object ,not mock object //Following code means, that as result of calling GetApplicationDirectory() //we will receive "C:\\Windows\\Fenestra" IloggerSomeDependency loggerDependency = Mock.Of<ILoggerSomeDependency>(d=>d.GetApplicationDirectory()=="C:\\Windows\\Fenestra"); var currentDirectory = loggerDependency.GetApplicationDirectory(); Assert.That(currentDirectory,Is.EqualTo("C:\\Windows\\Fenestra"));
- 方法
GetDirectoryForDependencyByLoggerName
的桩,总是返回相同的结果// For any argument of method GetDirectoryForDependencyByLoggerName return "C:\\Merced". ILoggerSomeDependency loggerDependency = Mock.Of<ILoggerSomeDependency> (ld => ld.GetDirectoryForDependencyByLoggerName(It.IsAny<string>()) == "C:\\Merced"); string directory = loggerDependency.GetDirectoryForDependencyByLoggerName("anything"); Assert.That(directory, Is.EqualTo("C:\\Merced"));
- 方法
GetDirrectoryByLoggerName
的桩,根据参数返回结果// Initialize stub with dependency from passed argument // into method GetDirrectoryByLoggerName // Code is similar to stub // public string GetDirectoryForDependencyByLoggerName(string s) { return "C:\\" + s; } Mock<ILoggerSomeDependency> stub = new Mock<ILoggerSomeDependency>(); stub.Setup(ld => ld.GetDirectoryForDependencyByLoggerName(It.IsAny<string>())) .Returns<string>(name => "C:\\" + name); string loggerName = "AnyLogger"; ILoggerSomeDependency logger = stub.Object; string directory = logger.GetDirectoryForDependencyByLoggerName(loggerName); Assert.That(directory, Is.EqualTo("C:\\" + loggerName));
- 属性
GetLoggerInstance
的桩// Property GetLoggerInstance of our stub will return pointed value ILoggerSomeDependency logger = Mock.Of<ILoggerSomeDependency>( d => d.GetLoggerInstance == "GetLoggerInstance"); string GetLoggerInstance = logger.GetLoggerInstance; Assert.That(GetLoggerInstance, Is.EqualTo("GetLoggerInstance"));
- 使用“mock 函数式规范”(v4版新增)通过一个表达式设置少数方法的行为
// Join stubs of different methods with help of logical and ILoggerSomeDependency logger = Mock.Of<ILoggerSomeDependency>( d => d.GetApplicationDirectory() == "C:\\Windows\\Fenestra" && d.GetLoggerInstance == "GetLoggerInstance" && d.GetDirectoryForDependencyByLoggerName (It.IsAny<string>()) == "C:\\Windows\\Temp"); Assert.That(logger.GetApplicationDirectory(), Is.EqualTo("C:\\Windows\\Fenestra")); Assert.That(logger.GetLoggerInstance, Is.EqualTo("GetLoggerInstance")); Assert.That(logger.GetDirectoryForDependencyByLoggerName ("CustomLogger"), Is.EqualTo("C:\\Windows\\Temp"));
- 使用
Setup
方法配置少数方法的行为(旧版或v3语法)var stub = new Mock<ILoggerSomeDependency>(); stub.Setup(ld => ld.GetApplicationDirectory()).Returns("C:\\Windows\\Fenestra"); stub.Setup(ld => ld.GetDirectoryForDependencyByLoggerName(It.IsAny<string>())).Returns("C:\\Windows\\Temp"); stub.SetupGet(ld => ld.GetLoggerInstance).Returns("GetLoggerInstance"); ILoggerSomeDependency logger = stub.Object; Assert.That(logger.GetApplicationDirectory(), Is.EqualTo("C:\\Windows\\Fenestra")); Assert.That(logger.GetLoggerInstance, Is.EqualTo("GetLoggerInstance")); Assert.That(logger.GetDirectoryForDependencyByLoggerName("CustomLogger"), Is.EqualTo("C:\\Windows\\Temp"));
注意
如前所述,Mock不区分模拟和桩,但对我们来说,区分初始化桩的语法会更容易。Mock函数式规范语法可用于测试状态条件(即用于桩),而不能用于配置行为。另一方面,使用Setup
方法初始化桩可能更麻烦,并且不总是容易掌握我们要检查的是行为还是状态。
行为验证
为了测试行为,我们将使用以下类和接口
public interface ILogSaver
{
string GetLogger();
void SetLogger(string logger);
void Write(string message);
}
public class Logger
{
private readonly ILogSaver _logSaver;
public Logger(ILogSaver logWriter)
{
_logSaver = logWriter;
}
public void WriteLine(string message)
{
_logSaver.Write(message);
}
}
- 检查类
Logger
的对象调用ILogSaver.Write
方法(使用任何参数)var mock = new Mock<ILogSaver>(); var logger = new Logger(mock.Object); logger.WriteLine("Greeting by logger!"); // Check that method Write was called of our Mock with any argument mock.Verify(lw => lw.Write(It.IsAny<string>()));
- 检查调用
ILogSaver.Write
方法并配置参数var mock = new Mock<ILogSaver>(); mock.Verify(lw => lw.Write("Greeting by logger!"));
- 检查
ILogSaver.Write
方法只调用了一次(不多不少)var mock = new Mock<ILogSaver>(); mock.Verify(lw => lw.Write(It.IsAny<string>()), Times.Once());
注意
有很多选项可以检查某个依赖项被调用了多少次。为此,您可以使用
Times
类中的以下方法:AtLeast(int)
、AtMost(int)
、Exactly
、Between
等。 - 使用
Verify
方法检查行为(您可以根据需要使用其他便捷方法,用于检查几个假设)var mock = new Mock<ILogSaver>(); mock.Setup(lw => lw.Write(It.IsAny<string>())); var logger = new Logger(mock.Object); logger.WriteLine("Greeting by logger!"); // We didn't pass into method Verify any additional parameters. // It means that method Verify will use expectations // configured with help of mock.Setup mock.Verify();
- 使用
Verify()
方法检查几次调用。
在某些情况下,使用Verify
的几个方法来检查几次调用会很方便。但是,您也可以使用mock对象,通过Setup
方法配置预期行为,然后通过调用一个Verify()
方法来检查这些假设。这种技术对于重复测试在Setup
方法配置中创建的Mock对象很有用。var mock = new Mock<ILogSaver>(); mock.Setup(lw => lw.Write(It.IsAny<string>())); mock.Setup(lw => lw.SetLogger(It.IsAny<string>())); var logger = new Logger(mock.Object); logger.WriteLine("Greeting by logger!"); mock.Verify();
说明笔记或严格与宽松模式
Mock支持两种行为检查模式:严格和宽松。默认情况下,使用宽松模式,这意味着被测类(CUT)在执行Act
阶段时可以调用依赖项的任何方法,我们不必指定所有这些方法。就像在前面的例子中一样,logger.WriteLine
方法调用了ILogSaver
接口的两个方法:Write
和SetLogger
。如果使用MockBehavior.Strict
,如果我们没有明确指定将调用哪些依赖项的方法,Verify
方法将失败。
var mock = new Mock<ILogSaver>(MockBehavior.Strict);
// if to comment any of the next lines
// then mock.Verify() will fail with exception
mock.Setup(lw => lw.Write(It.IsAny<string>()));
mock.Setup(lw => lw.SetLogger(It.IsAny<string>()));
var logger = new Logger(mock.Object);
logger.WriteLine("Greeting by logger!");
mock.Verify();
使用MockRepository
MockRepository
类提供了一种创建桩的另一种语法,更重要的是,它允许保留几个mock对象并通过调用一个方法来检查复杂行为。
- 使用
MockRepository.Of
创建桩语法类似于使用
Mock.Of
,但它允许通过使用几个where
方法而不是&&
运算符来设置不同方法的行为var repository = new MockRepository(MockBehavior.Default); ILoggerSomeDependency logger = repository.Of<ILoggerSomeDependency>() .Where(ld => ld.GetLoggerInstance == "GetLoggerInstance") .Where(ld => ld.GetApplicationDirectory() == "C:\\Windows\\Fenestra") .Where(ld => ld.GetDirectoryForDependencyByLoggerName (It.IsAny<string>()) == "C:\\Windows\\Temp") .First(); Assert.That(logger.GetApplicationDirectory(), Is.EqualTo("C:\\Windows\\Fenestra")); Assert.That(logger.GetLoggerInstance, Is.EqualTo("GetLoggerInstance")); Assert.That(logger.GetDirectoryForDependencyByLoggerName ("CustomLogger"), Is.EqualTo("C:\\Windows\\Temp"));
- 使用
MockRepository
设置几个mock对象的行为。假设您有一个更复杂的类WizzLogger
,它需要另外两个依赖项:ILogSaver
和ILogMailer
。我们的测试类在调用其Write
方法时,应该调用这两个依赖项的方法。例如,像这样
public interface ILogSaver { string GetLogger(); void SetLogger(string logger); void Write(string message); } public interface ILogMailer { void Send(MailMessage mailMessage); } public class WizzLogger { private ILogMailer mailer; private ILogSaver saver; public WizzLogger(ILogSaver s, ILogMailer m) { mailer = m; saver = s; } public void Send(MailMessage mailMessage) { } public void WriteLine(string message) { mailer.Send(new MailMessage()); saver.Write(message); } }
然后在您的测试中,您可以这样写
var repo = new MockRepository(MockBehavior.Default); var logWriterMock = repo.Create<ILogSaver>(); logWriterMock.Setup(lw => lw.Write(It.IsAny<string>())); var logMailerMock = repo.Create<ILogMailer>(); logMailerMock.Setup(lm => lm.Send(It.IsAny<MailMessage>())); var WizzLogger = new WizzLogger(logWriterMock.Object, logMailerMock.Object); WizzLogger.WriteLine("Hello, Logger"); repo.Verify();
其他方式
在某些情况下,根据接口获取Mock
对象本身(获取interface ISomething
的Mock<ISomething>
)可能很有用。例如,桩初始化的函数式语法返回的不是Mock
对象,而是直接返回所需的接口。这对于测试成对的简单方法可能很方便,但如果您还需要检查行为或配置返回不同参数的不同结果的方法,则不方便。因此,有时使用基于LINQ的语法来处理一部分方法,而使用Setup
方法处理另一部分方法会更容易。
ILoggerSomeDependency logger = Mock.Of<ILoggerSomeDependency>
(ld => ld.GetApplicationDirectory() == "C:\\Windows\\Fenestra"
&& ld.GetLoggerInstance == "GetLoggerInstance");
// Set more complicated behavior of method GetDirectoryForDependencyByLoggerName
// for returning different results depending from argument
Mock.Get(logger)
.Setup(ld => ld.GetDirectoryForDependencyByLoggerName(It.IsAny<string>()))
.Returns<string>(loggerName => "C:\\" + loggerName);
Assert.That(logger.GetApplicationDirectory(), Is.EqualTo("C:\\Windows\\Fenestra"));
Assert.That(logger.GetLoggerInstance, Is.EqualTo("GetLoggerInstance"));
Assert.That(logger.GetDirectoryForDependencyByLoggerName("Foo"),
Is.EqualTo("C:\\Merced"));
Assert.That(logger.GetDirectoryForDependencyByLoggerName
("Itanium"), Is.EqualTo("C:\\Itanium"));
此外,Mock还允许检查protected
方法的行为,测试事件,并包含其他功能。但这可能是另一篇文章的主题。
如果您在测试中感到困惑,我附上了接口和一些类的实现。