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

Rhino Mocks 的新版本

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.87/5 (19投票s)

2005 年 7 月 1 日

18分钟阅读

viewsIcon

157421

一个灵活的 mock 对象框架,以及对 mocking 的一般性讨论,现已推出第二版

Sample Image - Rhino_Mocks_Version_20.jpg

Rhino Mocks 版本 2.0

为 .Net 平台提供的动态 mock 对象框架。其目的是通过允许开发人员创建自定义对象的 mock 实现并通过单元测试验证交互来简化测试。

Rhino.Mocks 旨在创建一种更简单的方式来构建和使用 mock 对象,并为现有工具提供更好的重构支持。它采用混合方法,介于 EasyMock.Net 模型的纯 Record/Replay 和 NMock 的基于期望的模型之间。Rhino.Mocks 源自 EasyMock.Net,并尝试改进其模型,以创建易于使用且功能强大的 mocking 框架。它可供免费和商业软件免费使用和修改。

我创建 Rhino.Mocks 是因为我希望从 ReSharper 等重构工具中获得更好的支持,而且我不喜欢 NMock 处理参数化方法的方式(你需要传递假的约束才能让它识别正确的方法)以及缺乏可扩展性[我需要进行一些 hack 来达到我想要的结果]。

许可: Rhino Mocks 是自由软件,根据 BSD 许可证发布。

您可以在其 项目页面 下载 Rhino Mocks

Rhino Mocks 提供什么?

  • 显式的记录和回放模型用于期望。
  • 使用强类型 mock 对象进行工作。
  • 基于以下条件的期望:
    • 参数匹配
    • 约束匹配
    • 自定义回调,用于使用您自己的代码验证期望的参数
  • 设置方法的动作,返回特定值或抛出异常。

为什么要重复造轮子?

  • 其他框架,例如 NMockNMock2TypeMock.Net 使用基于字符串的方法进行 mocking 调用。这意味着您无法在运行时(如果能的话)利用编译器和大多数重构工具来捕获任何错误。在多次因此吃亏后,我决定我想要一个 mocking 框架,它使用对象而不是字符串来设置 mock 对象上的期望。
  • 为什么不使用 EasyMock.Net
    • EasyMock.Net 是一个 Java 库的端口,它使用 Remoting 代理来实现其功能。
    • 我不喜欢 EasyMock.Net 的 API,它在 .Net 中与 Java 有太多根源,用起来不舒服。
    • 我发现 Remoting 代理在几个地方表现得与真实对象完全一样。特别是,它们无法使用 Array.Copy() 复制;
    • 我对 Remoting 从未有过浓厚兴趣,所以我对此了解不多。

我希望您会喜欢使用 Rhino Mocks。

在第一篇 文章 中,我解释了 mock 对象、交互式测试、状态式测试等。这里是对不想阅读完整 文章 的人的简要回顾。

  • Mock 对象 - 一个模拟另一个对象的对象,它允许设置其与其他对象的交互的期望。
  • 交互式测试 - 您指定对象之间的特定交互序列,执行一个操作,然后验证交互序列是否按您指定的发生。
  • 状态式测试 - 您执行一个操作,然后检查预期的结果(返回值、属性、创建的对象等)。
  • 期望 - 用于验证特定方法调用是否是预期调用的通用名称。
  • 记录和回放模型 - 一个允许记录 mock 对象上的操作,然后回放和验证它们的模型。所有 mocking 框架都使用此模型。有些(NMock、TypeMock.Net、NMock2)隐式使用它,而有些(EasyMock.Net、Rhino Mocks)显式使用它。
  • 顺序 - 指定方法调用序列的重放是否按特定顺序(或无序)发生的可能性。

那么,有什么变化?

在 2.0 版本中,我从依赖于 Remoting 代理的 EasyMock.Net 代码迁移到了使用 Castle.DynamicProxy 的继承方法。生成的代码库更加灵活,易于扩展。在重建过程中,我注意到了几个因素。

  • NMock2 的语法 *非常* 好,并且允许对调用的方法设置清晰的期望。
  • 为我想使用的每个 mock 对象创建 MockControl 的麻烦。
  • 由于我没有验证每个 mock 对象的 MockControls,而导致假阳性测试的麻烦。
  • 保留指定约束、回调和参数匹配的能力。
  • 保留显式的记录和回放模型,但使其更易于使用。
  • 在代码中明确“最后调用”的概念。
  • 轻松创建 mock 对象并设置/验证它们的期望。
  • 如有需要,可轻松扩展框架。
  • 尽快失败,并附带描述性的错误消息。
  • mock 类和接口,并确保两者都*完全*按预期工作(*而不是**大致*按预期工作)。

Rhino Mocks 的能力

  • mock 接口和具体类,*包括具有参数化构造函数的类*。

  • 使用强类型 mock 对象而不是字符串来设置对调用方法的期望。
  • 易于重构和依赖编译器。
  • 允许对 mock 对象或多个 mock 对象设置广泛的期望。

好了,让我们来看代码吧?[我尝试为本文创建了一个示例,但该示例既牵强又未能正确描绘可能性。因此,这里的大部分代码示例都来自 NHibernate Query Analyzer 的测试,是“真实代码”。]

mock 对象是为了让您能够测试组件之间的*交互*,这在测试难以进行状态式测试的代码时非常有用。这里的大部分示例是检查视图的保存例程是否按预期工作的测试。此方法的要求是:

  • 如果项目是新项目,则要求提供项目名称。[允许此时取消保存]
    • 如果新项目名称已存在,询问用户是否要覆盖。[如果否,则取消保存操作]
    • 如果用户批准覆盖,则删除旧项目。
  • 保存项目。

所以这里有五个测试:

  1. 项目是新项目,用户在新名称处取消。
  2. 项目是新项目,用户提供了新名称,项目已保存。
  3. 项目是新项目,用户提供了已存在的名称,但未批准覆盖。
  4. 项目是新项目,用户提供了已存在的名称,并批准覆盖。
  5. 项目已存在,因此只需保存。

尝试使用状态式测试进行测试将是尴尬而痛苦的,而使用交互式测试,这将是轻而易举的(其他类型的测试使用交互式测试进行测试将同样痛苦,但使用状态式测试则很容易)。

说够了,让我们看一些代码

[Test]
public void SaveProjectAs_CanBeCanceled()
{
    using(MockRepository mocks = new MocksRepository())
    {
        IProjectView projectView = (IProjectView)projectView.CreateMock(typeof(IProjectView));
        Project prj = new Project("Example Project");
        IProjectPresenter presenter = new ProjectPresenter(prj,projectView);
        Expect.On(projectView).Call(projectView.Title).Return(prj.Name);
        Expect.On(projectView).Call(projectView.Ask(question,answer)).Return(null);
        mocks.ReplayAll();
        Assert.IsFalse(presenter.SaveProjectAs());            
    }
}

我们创建一个 MockRepository,然后创建一个 mock 项目视图、项目和一个项目表示器,然后设置期望。完成设置期望后,我们进入回放状态,并调用 SaveProjectAs() 方法,如果用户取消保存过程,该方法应返回 false。

这个例子阐明了 Rhino Mocks 的几个关键概念。

  • 我们使用对象的方法而不是字符串来设置对象上的期望,这减少了在运行时(希望如此)运行测试时出现错误的几率。
  • 我们使用显式调用从 Record 状态切换到 Replay 状态。
  • MockRepository 实现 IDisposable;因此,当我们到达 using 语句的末尾时。

这是最简单的例子,真正的测试将 MockRepository、项目、视图和表示器的创建移至 setup 方法,因为它们是每个测试所必需的。您可以看到我们期望两个方法被调用,并带有特定参数,并且我们为每个方法设置了结果。此方法使用参数匹配期望,Rhino Mocks 支持更多。更多信息:方法调用

顺序 / 无序

Rhino Mocks 中的方法调用可以是顺序的或无序的。记录器的默认状态是无序记录,这意味着在回放期间,方法可以按任何顺序出现。如果记录器更改为有序,则方法必须按与记录时完全相同的顺序调用。下面是一个代码示例

[Test]
public void SaveProjectAs_NewNameWithoutConflicts()
{
    using(mocks.Ordered())
    {
        Expect.On(projectView).
            Call(projectView.Title).
            Return(prj.Name);
        Expect.On(projectView).
            Call(projectView.Ask(question,answer)).
            Return( newProjectName);
        Expect.On(repository).
            Call(repository.GetProjectByName(newProjectName)).
            Return(null);
        projectView.Title = newProjectName;
        projectView.HasChanges = false;
        repository.SaveProject(prj);
    }
    mocks.ReplayAll();
    Assert.IsTrue(presenter.SaveProjectAs());
    Assert.AreEqual(newProjectName,prj.Name);
}

在上面的代码示例中,我们要求 Rhino Mocks 验证调用是否按完全相同的顺序发生。请注意,我们对多个 mock 对象设置了期望,Rhino Mocks 将处理它们之间的顺序。这意味着如果我在从存储库获取项目之前设置项目视图标题,测试将失败。在 Rhino Mocks 中,默认使用无序匹配,但它支持有序和无序之间无限深度的嵌套,这意味着您可以创建非常强大的期望。这是一个牵强示例

[Test]
public void MovingFundsUsingTransactions()
{
    using(MockRepository mocks = new MockRepository())
    {
        IDatabaseManager databaseManager = mocks.CreateMock(typeof(IDatabaseManager));
        IBankAccount accountOne = mocks.CreateMock(typeof(IBackAccount)),
            accountTwo = mocks.CreateMock(typeof(IBankAccount)); 
        using(mocks.Ordered())
        {
            Expect.On(databaseManager).
                Call(databaseManager.BeginTransaction()).
                Return(databaseManager);
            using(mocks.Unordered())
            {
                accountOne.Withdraw(1000);
                accountTwo.Deposit(1000);
            }
            databaseManager.Dispose();
        }
        mocks.ReplayAll();
    
        Bank bank = new Bank(databaseManager);
        bank.TransferFunds(accountOne,accountTwo,1000);
    }
}

此代码验证将资金从一个账户转移到另一个账户的操作是否包装在事务中,但实现可以先从第一个账户提取,或者先存入第二个账户,两者都是合法的,只要两个操作都发生。反之亦然,您可以指定无序的有序事件序列(我希望 A 然后 B 然后 C 发生,并且我希望 D 然后 E 然后 F 发生,但我不在乎哪个序列先发生)。

顺序有两个注意事项:

  • 要退出回放状态的排序,您必须调用所有已记录的方法。在上面的示例中,我们只能在调用了两个方法之后才能退出内部的 Unordered 排序(包含提款和存款的那一个)。这与记录代码的外观一致,因此不应造成任何意外。
  • 在开始回放之前,您必须退出所有排序。这由框架强制执行(如果您尝试,会收到一个异常)。

IDisposable 模式

using(...) 语法和 IDisposable 在确保您不忘记清理资源方面非常方便,在 Rhino Mocks 中,它在两个地方使用:

  • MockRepository 实现 IDisposable,这样您就不会忘记调用 VerifyAll() 或 ReplayAll()。
    • 如果在测试过程中由于 Rhino Mocks 抛出了异常,存储库将不会验证,您将获得抛出的异常。
    • 但是,如果抛出的异常不是源自 Rhino Mocks(例如,由于断言失败),则存储库将尝试验证所有期望是否已满足,并(很可能)失败。这将导致一个掩盖了实际失败原因的异常。请记住这一点,如果您有一个失败的测试。
    • 推荐的方法是在 Setup 方法中创建存储库,并在 Teardown 方法中验证它,这样您就能获得正确的异常。只需在 teardown 中调用 VerifyAll()(或 Dispose(),它执行相同操作)。
  • MockRepository 上的 Ordered() 和 Unordered() 方法,这两个方法将 MockRepository 设置为按顺序或无序方式期望下一个方法。这些调用应包装在 using 声明中(或手动调用 Dispose())。您不能在开放的排序调用中开始回放。

方法调用

当 Rhino Mocks 拦截对 mock 对象的调用时,它会通过检查方法签名、调用该方法的对象以及期望在此方法上是否与传递给方法的参数匹配来确定该调用是否被期望。对 mock 对象和方法签名的匹配并不十分有趣,但对方法调用的期望则很有趣。Rhino Mocks 支持以下期望:

  • 匹配参数 - 为了匹配,在回放状态中必须传递与记录状态中传递的参数相同的参数。有一个注意事项,对于数组,比较的是数组中包含的对象。这是 Rhino Mocks 的默认行为,无需您采取任何操作。
  • 忽略参数 - 只期望在此对象上调用方法,接受任何参数。如果您不关心参数,这很有用。要激活此行为,您需要调用 IMethodOptions 接口上的 IgnoreArguments()(更多信息:方法选项接口)。
  • 约束匹配 - 每个参数都根据约束进行验证,要接受调用,所有约束都必须通过。(更多信息:约束。)

  • 回调 - 调用用户提供的委托来验证方法调用是否有效,如果委托返回 true,则接受该方法。(更多信息:回调。)

回调

回调是用户提供的委托,每当 Rhino Mocks 需要评估方法调用是否被期望时,就会调用它。这在您想要对方法参数进行复杂验证,或者需要 mock 测试中对象之间的复杂交互的情况下非常有用。我添加此功能是因为我想测试一些具有以下语义的多线程代码:启动作业,并在完成后通知我。在不引入真实线程(并破坏测试隔离)的情况下重新创建它的唯一方法是在回放状态期间插入我自己的代码并自己调用被测试的对象。

在决定使用回调之前需要考虑的一些事项:

  • 它很容易被滥用。使用回调的测试可能很难理解,因为您会收到来自本应无害代码的调用。只有在需要时才这样做。
  • 您的回调可能会(并且很可能)被调用多次;在编写时请记住这一点。要么将其全部包装在 if ( firstTimeCalled ) { /*do work*/ } 中,要么确保对委托的重复调用不会产生任何糟糕的副作用。

如果它如此容易被滥用,为什么还要添加它?

  • 因为当您需要它时,您*确实*需要它,而且我宁愿有一个 nice 方法来做这件事,而不是一个非常丑陋hack

  • 因为我尊重那些将使用此框架的人,他们不会滥用它。

技术细节 - 要成为有效回调,回调必须返回一个 Boolean,并且具有与 mock 方法相同的参数。您使用以下代码注册委托:

IProjectRepository repository = (IProjectRepository) mocks.CreateMock(typeof(IProjectRepository));
IProjectView view = (IProjectView )mocks.CreateMock(typeof(IProjectView ));
Expect.On(view).(view.Ask(null,null)).Callback(new AskDelegate(DemoAskDelegateMethod)).Return(null);

请注意,您无法更改方法的返回值,但必须显式传递它。

递归期望

关于回调的最后一条警告。如果您的回调会发起一个导致对 mock 对象*其他*调用的操作,那么当与 Ordered() 混合时,这将失败。原因是框架无法决定是否接受调用,而评估时调用另一个 mock 方法意味着当前正在评估的调用仍然是当前正在评估的调用。这将导致测试失败。使用 Unordered(),它将起作用,因为第二个期望不依赖于第一个期望首先被接受。简而言之,Ordered() 不可重入 :-)

方法选项接口

IMethodOptions 允许您为方法调用设置各种选项。下面是一个告诉 Rhino Mocks 忽略方法参数的示例:

IProjectRepository repository = (IProjectRepository)mocks.CreateMock(typeof(IProjectRepository));
IProjectView view = (IProjectView ) mocks.CreateMock(typeof(IProjectView ));
Expect.On(view).(view.Ask(null,null)).IgnoreArguments().Return(null);
repository.SaveProject(null);
LastCall.On(repository).IgnoreArguments();

正如您所见,我们使用 Expect.On().Call() 来处理有返回值的函数,并使用 LastCall.On() 来处理返回 void 的函数以获取 IMethodOptions 接口。我认为 Expect.On().Call() 语法稍微清晰一些,但两者之间没有实际区别。我建议尽可能使用 Expect(任何有返回值的)。对于属性设置器或返回 void 的方法,Expect 语法不适用,因为没有返回值。因此,需要 LastCall.On()。“最后调用”的概念在记录状态中无处不在,您只能为最后一次调用设置方法选项 - 即使是 Expect.On().Call() 语法也只是 LastCall.On() 的包装。

Expect.On().Call() 和 LastCall.On() 允许您设置以下选项:

  • 方法的返回值(如果存在)。

Expect.On(view).(view.Ask(null,null)).
    Return(null);
  • 方法将抛出的异常

Expect.On(view).(view.Ask(null,null)).
    Throw(new Exception("Demo"));
  • 期望此方法重复的次数(那里有一些便利方法)。

Expect.On(view).(view.Ask(null,null)).Return(null).
    Repeat.Twice();
  • 忽略方法参数

Expect.On(view).(view.Ask(null,null)).Return(null).
    IgnoreArguments();
  • 设置方法的约束

Expect.On(view).(view.Ask(null,null)).Return(null)
    .Constraints(
        Text.StartsWith("Some"),
        Text.EndsWith("Text"));
  • 设置此方法的回调

Expect.On(view).(view.Ask(null,null)).Return(null).
    Callback(new AskDelegate(VerifyAskArguments));

注意:对于返回值的方法,您*必须*指定一个返回值或一个要抛出的异常。否则,您将无法继续记录或进入回放状态。

注意 II:方法链式调用确实使编写此代码更加容易。

约束

约束是验证方法参数是否符合特定条件的一种方式。Rhino Mocks 包含一些内置约束,并允许您定义自己的自定义约束,它将与框架干净地集成。您可以使用以下语法为方法参数指定约束:

Expect.On(view).
    (view.Ask(null,null)).Return(null).
    Constraints( 
        Text.StartsWith("Some"),
        Text.EndsWith("Text"));

您需要传递与方法参数数量完全相同的约束数量。当在回放状态期间调用方法时,Rhino Mocks 会根据相同索引处的参数评估每个约束,并且如果所有约束都满足,则接受该方法。顺便说一句,我从 NMock2 获取了当前约束语法的想法,它是创建约束的一种更简洁的方法。

Rhino Mocks 内置约束

Rhino mocks 随附以下约束:

约束

示例 接受值(s) 不接受值(s)
对象 任意 Is.Anything() 任何内容 {0,"","whatever",null, etc} (无!)
等于 Is.Equal(3) (int)3 3f, 4,"", new object()
不等于 Is.NotEqual(3) 或 !Is.Equal(3) 3f,43,null,DateTime.Now (int)3
Null (空值) Is.Null() null 5,"",new object()
非空 Is.NotNull() DateTime.Now, "asbcd", 0xHead null
类型 Is.TypeOf(typeof(string)) "","Hello",String.Empty null, 3, DateTime.Now
大于 Is.GreaterThan(10) 15,100,300 3,4,5, 10
大于等于 Is.GreaterThanOrEqual(10) 10,15,100 4,2,8,9
小于 Is.LessThan(10) 1,2,9 10,32,100
小于等于 Is.LessThanOrEqual(10) 1,9,10 11,33,43
属性 等于值 Property.Value("Length",0) new ArrayList(),String.Empty "Hello", 5
Null (空值) Property.IsNull("InnerException") new Exception("exception without inner exception") new Exception("Exception with inner Exception", new Exception("Inner")
非空 Property.IsNotNull("InnerException") new Exception("Exception with inner Exception", new Exception("Inner") new Exception("exception without inner exception")
列表 在列表中
[参数是一个包含此值的集合]
List.IsIn(4) new int[]{1,2,3,4}, new int[]{4,5,6} new object[]{"",3}
其中一个
[参数等于此列表中的一个对象]
List.OneOf(new int[]{3,4,5}) 3,4,5 9,1,""
等于 List.Equal(new int[]{4,5,6}) new int[]{4,5,6}, new object[]{4,5,6} new int[]{4,5,6,7}
文本 以...开头 Text.StartsWith("Hello") "Hello, World", "Hello, Rhino Mocks" "", "Bye, Bye"
以...结尾 Text.EndsWith("World") "World","Champion Of The World" "world", "World Seria"
Contains Text.Contains("or") "The Horror Movie...", "Either that or this" "Movie Of The Year"
例如
[执行正则表达式验证]
Text.Like("Rhino|rhino|Rhinoceros|rhinoceros" ) "Rhino Mocks", "Red Rhinoceros" "Hello world", "Foo bar", Another boring example string"
运算符重载 AND - 运算符 & Text.StartsWith("Hello") & Text.Contains("bar") "Hello, Foobar" "Hello, World", "Foo bar"
OR - 运算符 | Text.StartsWith("Hello") & Text.Contains("bar") "Hello, Foobar", "Hello, World", "Foo bar" "boring string"
NOT - 运算符 ! !Text.StartsWith("Hello") "Foo bar", "index.html" "Hello, Rhino Mocks"

运算符重载警告:如果您在一个语句中使用多个运算符,请注意运算符的优先级,否则可能会得到意外的结果。

创建自定义约束:创建自定义约束很容易,只需从 AbstractConstraint 派生一个类,并在 Eval() 方法中返回 true 即可。

设置结果

有时您有一个 mock 对象上的方法,您不关心它是如何/是否被调用的,您可能想设置一个返回值(或一个要抛出的异常),但对于这个特定的测试,您 just 不关心。例如,您可能有一些已经验证过的交互,或者您正在测试其他类,只需要从某个方法中获取正确的值。在 Rhino Mocks 中做到这一点的方法是使用 SetupResult.On().Call()。代码如下:

[Test]
public void SetupResultUsingOrdered()
{
    SetupResult.On(demo).Call(demo.Prop).Return("Ayende");
    using(mocks.Ordered())
    {
        demo.VoidNoArgs();
        LastCall.On(demo).Repeat.Twice();
    }
    mocks.ReplayAll();
    demo.VoidNoArgs();
    for (int i = 0; i < 30; i++)
    {
        Assert.AreEqual("Ayende",demo.Prop);                
    }
    demo.VoidNoArgs();
}

当使用 SetupResult.On().Call() 时,我们告诉 Rhino Mocks:“我不在乎这个方法,它需要做 X,所以就这么做,但除此之外就忽略它。”使用 SetupResult.On().Call() 完全绕过了 Rhino Mocks 中的期望模型。在上面的示例中,我们定义 demo.Prop 返回“Ayende”,无论当前期望的方法是什么,我们都可以调用它。

那返回 void 的方法呢?
您将使用 LastCall.On().Repeat.Any(),它具有相同的语义(忽略顺序等)。

注意:如果您想有一个可以重复任意次数但仍遵守顺序的方法,您可以使用:LastCall.On().Repeat.Times(0,int.MaxValue),这确实遵守了所有正常规则。

Mocking 类

Rhino Mocks 支持 mock 具体类和接口。事实上,它甚至可以 mock 没有默认构造函数的类!要 mock 一个类,只需将它的类型传递给 MockRepository.CreateMock(),并提供构造函数的任何参数。

[Test]
public void AbuseArrayList()
{
    using(MockRepository mocks = new MockRepository())
    {
        ArrayList list = (ArrayList)mocks.CreateMock(typeof(ArrayList));
        Expect.On(list).Call(list.Capacity).Return(999);
        mocks.ReplayAll();
        Assert.AreEqual(999,list.Capacity);
    }
}

如果您想调用非默认构造函数,只需在类型后面添加参数。像这样:

ArrayList list = (ArrayList)mocks.CreateMock(typeof(ArrayList),500);

需要注意的一些事项:

  • 您无法从密封类创建 mock 对象。
  • 您无法拦截对非虚方法的调用。

限制:

  • 目前 Rhino Mocks 无法 mock 使用 out 或 ref 参数的接口和类。
© . All rights reserved.