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

Rhino Mocks 的 Remoting 代理支持

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (5投票s)

2007年12月17日

CPOL

7分钟阅读

viewsIcon

25703

Rhino Mocks 3.3 版本可以模拟任何 MarshalByRefObject。

摘要

2007 年 10 月,我为 Ayende RahienRhino Mocks 库添加了 Remoting proxy 支持。这使得 Rhino Mocks 可以模拟任何继承自 MarshalByRefObject 的类型,例如 AppDomainControl。此功能从 Rhino Mocks 3.3 开始可用。

一些历史

在我为 Bloomberg 工作期间,我们使用了 NMock 库的一个名为 "ProxyMock" 的扩展。这个扩展由我的朋友 Levy Haskell 编写。ProxyMock 使用 .NET Remoting 机制来模拟任何 MarshalByRefObject。ProxyMock 的当前状态我不太清楚。据我所知,它曾经是 NMock 代码库的一部分,但从未在 NMock 的公开版本中发布。

离开 Bloomberg 后,我无法再使用 ProxyMock。我在 Finetix 的朋友们向我介绍了 Ayende RahienRhino Mocks。我立刻爱上了这个库,但我却怀念模拟 MarshalByRefObject 的能力。

不可模拟的类

偶尔,我会在测试中遇到一些棘手的情况。假设我有一个包含 Infragistics 网格的视图。每次网格行收到新数据时,Infragistics 网格都会触发一个事件。订阅者可以拦截此事件并根据新数据更改行外观,例如更改背景颜色。视图类将行初始化事件传递给演示器,演示器告诉视图使用哪种背景颜色。

class InitializeRowEventArgs
{
    UltraGridRow GridRow; // from Infragistics grid library
    DataRow DataRow;
}

delegate void InitializeRowHandler(object sender, 
              InitializeRowEventArgs row);

interface IView
{
    event InitializeRowHandler RowInitialized;
    void SetBackgroundColor( UltraGridRow gridRow, Color color );
}

class Presenter
{
    IView _view;
    
    ...
    _view.RowInitialized += OnRowInitialized;
    ...
    
    void OnRowInitialized( object sender, InitializeRowEventArgs args )
    {
        _view.SetBackgroundColor(args.GridRow, GetColorForRow(args.DataRow));
    }
}

演示器并不真正关心 UltraGridRow 的内部细节。在此特定示例中,对于演示器来说,网格行完全是不透明的。另一方面,演示器需要一个网格行对象。否则,它将无法告诉视图要着色哪一行。

编写此代码的测试时,我需要创建一个网格行对象,将其传递给演示器,并确保初始化的行是着色的行。

[TestFixture]
public class PresenterTest
{
    MockRepository _mocks;
    
    [SetUp]
    public void Setup()
    {
       _mocks = new MockRepository();
    }
    
    [Test]
    public void ColorRowOnInit()
    {
        IView view = _mocks.CreateMock<IView>();
        view.RowInitialized += null;
        IEventRaiser rowInitialized = 
           LastCall.IgnoreArguments().GetEventRaiser();
        UltraGridRow fakeGridRow = what goes here?
        DataRow dataRow = some sample data;
        RowInitializedEventArgs args = 
           new RowInitializedEventArgs(fakeGridRow, dataRow);
        
        view.SetBackgroundColor( fakeGridRow, Color.Red );
        // we expect it to be red
        
        _mocks.ReplayAll();
        
        Presenter = new Presenter(view);
        rowInitialized.Raise(view, args);
        
        _mocks.VerifyAll();
    }
}

问题是,我无法轻易创建 UltraGridRow 对象:它没有公共构造函数。要获取一个真正的 UltraGridRow,我需要创建一个真实的 Infragistics 网格控件,对其进行正确设置,并为其提供至少一行数据的真实数据源。对于一个小测试来说,这太麻烦了。

我也无法模拟 UltraGridRow。它不是接口,而是真实类。Rhino Mocks 通过动态创建重写了虚拟函数的派生类来模拟真实类。如果我无法创建 UltraGridRow,那么创建派生类型对象也将是一个问题。

Remoting 来拯救

幸运的是,UltraGridRow 和许多其他图形控件最终都继承自 MarshalByRefObject。这意味着,我可以对其进行一些 Remoting 操作。典型的远程对象工作方式如下。

remoting proxy picture

透明代理

客户端没有真实对象。一个称为“透明代理”的特殊实体取而代之。透明代理是一个非常特殊的对象。给定任何继承自 MarshalByRefObject 的类型 T,.NET 框架就可以凭空构造该类型的“伪”实例,绕过任何构造函数和初始化程序。显然,这在常规 C#(甚至 IL)中是无法做到的。透明代理是由“魔术”方法 RemotingServices.CreateTransparentProxy() 创建的。此方法被标记为 extern,并在非托管代码中实现。它也是内部的。普通人通过 RealProxy.GetTransparentProxy() 间接调用此方法。

由于透明代理没有“实体”,它无法执行任何实际工作。它通过调用 RealProject.Invoke() 方法将所有方法调用转发给真实代理对象。真实代理负责打包输入参数,通过网络传输它们,接收返回消息,处理结果,并将它们转换为返回值或异常。

真实代理

System.Runtime.Remoting.Proxies 命名空间中的 RealProxy 类是所有真实代理的基类。RealProxy 类是抽象的。实际的 .NET Remoting 代理是 System.Runtime.Remoting.Proxies.RemotingProxy。但是,我们可以自由创建自己的真实代理,它们可以执行任何其他类型的封送。

Real Proxy and derived classes

模拟代理

如果你阅读了透明代理的描述,它看起来就像是专门为创建模拟而发明的。它可以凭空构造一个伪对象,并将所有调用转发给一个特殊的拦截器——真实代理。

唯一的问题是创建一个这样的拦截器。在 2007 年 7 月底,我问 Ayende 他是否对此类拦截器感兴趣,他说感兴趣。在 2007 年 10 月,我终于有时间来编写它了。我和 Ayende 经过了几轮讨论,结果——Rhino Mocks 3.3 就具备了模拟 MarshalByRefObject 的能力。

Rhino Mocks 在内部使用了 Castle project 的拦截框架。该框架定义了 IInvocation 接口来表示单个方法调用,以及 IInterceptor 接口来表示调用拦截器。Castle 没有定义拦截器应该具体做什么。Rhino Mocks 定义了 RhinoInterceptor 类,它处理模拟期望和回放。

我需要编写一个适配器,将调用从 .NET Remoting 代理转发到 IInterceptor。这被证明是相对直接的,但在此过程中也遇到了一些棘手的陷阱。你可以在 Rhino Mocks 的 SVN 仓库中看到源代码。

Mocking proxy

必须牢记,绝对所有方法调用都会被转发到真实代理。这包括 GetType()GetHashCode()Equals() 等方法。如果你想将代理放入容器(Rhino Mocks 确实想这样做),这些方法不能被忽略。它们必须在代理级别处理,并且必须正确处理。

与真实代理通信

另一个棘手的问题是在只有透明代理实例的情况下检索关于真实代理的信息。特别是,我需要提取 Rhino Mocks 真实代理持有的 IMockedObject 引用。IMockedObject 是一个内部的 Rhino Mocks 类,用于家务管理。

问题是:透明代理可以是任何类型(只要它继承自 MarshalByRefObject),所以我只能依赖 Object 类的那些方法。可供选择的不多。

public class Object
{
    public virtual bool Equals(object obj);
    public virtual int GetHashCode();
    public Type GetType();
    public virtual string ToString();
}

其中,GetHashCode()GetType()ToString() 看起来并不太有趣,因为

  1. 它们不接受任何参数,并且
  2. 它们返回某种固定类型。

我无法通过这三种方法中的任何一种来检索 IMockedObject 引用。这样就剩下 Equals() 了。Equals() 可以接受任何类型的对象,并且在必要时,它甚至可以修改该对象来存储返回数据。因此,我可以使用 Equals() 作为双向通信机制,它可以告诉真实代理我想要什么,并获取回数据。当然,这并不是 Equals() 的设计初衷。通常,Equals() 不应该修改其参数。Ayende 将这种技术称为“邪恶的 hack”。然而,这是从真实代理中提取数据的唯一可行方法。

我甚至为这个邪恶的 hack 添加了一些面向对象的风味——类似于访问者模式。我定义了一个特殊的接口

interface IRemotingProxyOperation
{
    void Process(RemotingProxy proxy);
}

真实代理的 Equals() 处理程序会检查传入的对象是否为 IRemotingProxyOperation 类型,如果是,则调用其 Process() 方法,并将代理作为参数传递。

private bool HandleEquals(IMethodMessage mcm)
{
    object another = mcm.Args[0];
    if (another == null) return false;

    if (another <is IRemotingProxyOperation)
    {
        ((IRemotingProxyOperation)another).Process(this);
        return false;
    }
    return ReferenceEquals(GetTransparentProxy(), another);
}

这允许在不修改代理代码的情况下添加新的代理处理操作。目前,只定义了两个操作:检查特定对象是否是 Rhino Mocks Remoting 代理的透明代理,以及检索 IMockedObject 引用(RemotingMockGenerator.cs)。

public static bool IsRemotingProxy(object obj)
{
    if (obj == null) return false;
    RemotingProxyDetector detector = new RemotingProxyDetector();
    obj.Equals(detector);
    return detector.Detected;
}

public static IMockedObject GetMockedObjectFromProxy(object proxy)
{
    if (proxy == null) return null;
    RemotingProxyMockedObjectGetter getter = new RemotingProxyMockedObjectGetter();
    proxy.Equals(getter);
    return getter.MockedObject;
}

使用 Remoting Proxy

你不需要做任何特别的事情来使用 Remoting proxy。这就是为什么这段落如此简短。:-) CreateMock<T>() 方法会自动检测你尝试模拟的类型,并在类型是 MarshalByRefObject 时使用 Remoting proxy。例如。

MockRepository mocks = new MockRepository();
UltraGridRow row = mocks.CreateMock<UltraGridRow>(); // uses remoting mock

结论

将 Remoting proxies 添加到 Rhino Mocks 扩展了它可以模拟的类型集合。许多“棘手”的类型都继承自 MarshalByRefObject。特别是 System.Windows.Forms.Control 及其所有派生类都按引用封送,因此可以使用 Remoting proxies 进行模拟。大多数 Infragistics 类型(在 Infragistics 的 Windows Forms 版本中)也按引用封送,因此也可以进行模拟。

有些人说这会将 Rhino Mocks 推向 TypeMock 的方向,并认为这是坏事。理论上可能是这样,但实际上,Remoting mocks 让我能够单元测试以前无法进行单元测试的东西。所以,至少在某些情况下,它必须是件好事。

此外,参与这个项目很有趣,我学到了很多关于 Rhino Mocks 内部、泛型类型反射等方面的知识。我希望你将同样享受这个项目的最终成果,就像我享受开发它一样。

历史

  • 最后更新:2007 年 12 月 17 日。
© . All rights reserved.