65.9K
CodeProject 正在变化。 阅读更多。
Home

Mock 用法示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (6投票s)

2016年5月11日

CPOL

4分钟阅读

viewsIcon

12547

downloadIcon

109

Mock 用法示例

引言

Mock 是一个简单轻量级的隔离框架,它建立在匿名方法和表达式树的基础上。为了创建它们,它使用了代码生成,从而允许模拟接口、虚方法(甚至受保护的方法),但不允许模拟非虚方法和static方法。

注意

市场上只有两个框架允许模拟一切。它们是TypeMockIsolator和Microsoft Fakes,可用于Visual Studio 2012及更高版本。这些框架与Mock(使用代码生成)不同,它们使用CLR Profiling API,允许模拟任何方法,包括staticvirtualprivate方法。恕我直言,它们对于测试难以或不可能一次性重构的遗留代码很有用。

在Mock中,桩(stubs)和模拟(mocks)之间没有区别。或者更正式地说,状态验证和行为验证之间没有区别。尽管区分它们并非总是一件容易的事,但很多时候同一个元素可以扮演两种角色。我们将从简单到复杂的例子进行探讨。起初,我们将考虑状态验证,之后,我们将切换到行为验证。

状态验证

例如,我们将考虑以下接口的一组单元测试

public interface ILoggerSomeDependency
{
 	string GetApplicationDirectory();
	string GetDirectoryForDependencyByLoggerName(string loggerName);
	string GetLoggerInstance{get;}
} 
  1. 方法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"));
  2. 方法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"));
  3. 方法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));
  4. 属性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"));
  5. 使用“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"));
  6. 使用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);
    }
}
  1. 检查类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>()));
  2. 检查调用ILogSaver.Write方法并配置参数
    var mock = new Mock<ILogSaver>();
    mock.Verify(lw => lw.Write("Greeting by logger!"));
  3. 检查ILogSaver.Write方法只调用了一次(不多不少)
    var mock = new Mock<ILogSaver>();
    mock.Verify(lw => lw.Write(It.IsAny<string>()), Times.Once());

    注意

    有很多选项可以检查某个依赖项被调用了多少次。为此,您可以使用Times类中的以下方法:AtLeast(int)AtMost(int)ExactlyBetween等。

  4. 使用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();
  5. 使用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接口的两个方法:WriteSetLogger。如果使用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对象并通过调用一个方法来检查复杂行为。

  1. 使用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"));
  2. 使用MockRepository设置几个mock对象的行为。假设您有一个更复杂的类WizzLogger,它需要另外两个依赖项:ILogSaverILogMailer。我们的测试类在调用其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 ISomethingMock<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方法的行为,测试事件,并包含其他功能。但这可能是另一篇文章的主题。

如果您在测试中感到困惑,我附上了接口和一些类的实现。

© . All rights reserved.