使用moq模拟第三方回调事件

Joh*_*bly 7 c# nunit unit-testing moq mocking

我们一直在尝试使用C#编写的worker类的单元测试,它使用moq模拟第三方API(基于COM)来动态创建模拟对象.NUnit是我们的单元测试框架.

这个第三方组件实现了几个接口,但还需要使用事件回调到我们的工作类.我们的计划是模拟第三方组件可以引发的事件,并测试我们的工人类是否按预期运行.

不幸的是,我们遇到了一个问题,因为moq似乎无法模拟并引发外部定义的事件.遗憾的是,我无法提供我们正在使用的确切第三方API的代码,但我们使用MS Word API重新创建了该问题,并且还显示了在使用本地定义的界面时测试如何工作:

using Microsoft.Office.Interop.Word;
using Moq;
using NUnit.Framework;
using SeparateNamespace;

namespace SeparateNamespace
{
    public interface LocalInterface_Event
    {
        event ApplicationEvents4_WindowActivateEventHandler WindowActivate;
    }
}

namespace TestInteropInterfaces
{
    [TestFixture]
    public class Test
    {
        [Test]
        public void InteropExample()
        {
            // from interop
            Mock<ApplicationEvents4_Event> mockApp = new Mock<ApplicationEvents4_Event>();

            // identical code from here on...
            bool isDelegateCalled = false;

            mockApp.Object.WindowActivate += delegate { isDelegateCalled = true; };

            mockApp.Raise(x => x.WindowActivate += null, null, null);

            Assert.True(isDelegateCalled);
        }

        [Test]
        public void LocalExample()
        {
            // from local interface
            Mock<LocalInterface_Event> mockApp = new Mock<LocalInterface_Event>();

            // identical code from here on...
            bool isDelegateCalled = false;

            mockApp.Object.WindowActivate += delegate { isDelegateCalled = true; };

            mockApp.Raise(x => x.WindowActivate += null, null, null);

            Assert.True(isDelegateCalled);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

任何人都可以解释为什么为本地定义的界面引发事件但不能从第三方API(在本例中为Word)中导入的事件?

我有一种感觉,这与我们与COM对象(通过互操作程序集)交谈的事实有关,但我不确定如何解决问题.

g t*_*g t 14

Moq通过检测对事件内部方法的调用来"拦截"事件.这些方法被命名为add_+事件名称,并且是"特殊的",因为它们是非标准的C#方法.事件有点像属性(get/ set),可以定义如下:

event EventHandler MyEvent
{
    add { /* add event code */ };
    remove { /* remove event code */ };
}
Run Code Online (Sandbox Code Playgroud)

如果在Moq'd的接口上定义了上述事件,则会使用以下代码来引发该事件:

var mock = new Mock<IInterfaceWithEvent>;
mock.Raise(e => e.MyEvent += null);
Run Code Online (Sandbox Code Playgroud)

由于在C#中不可能直接引用事件,因此Moq拦截Mock上的所有方法调用并测试调用是否要添加事件处理程序(在上面的例子中,添加了一个空处理程序).如果是这样,则可以间接获得引用作为该方法的"目标".

Moq使用反射作为从名称开始的方法add_IsSpecialName标志集来检测事件处理程序方法.这个额外的检查是过滤掉与事件无关但名称起始的方法调用add_.

在该示例中,将调用截获的方法add_MyEventIsSpecialName设置标志.

然而,似乎这是不适合的互操作性展示定义的接口完全正确的,因为虽然事件处理程序方法的名称开头add_,它并没有IsSpecialName标志设置.这可能是因为事件正在通过较低级别的代码编组到(COM)函数,而不是真正的"特殊"C#事件.

这可以通过以下NUnit测试显示(按照您的示例):

MethodInfo interopMethod = typeof(ApplicationEvents4_Event).GetMethod("add_WindowActivate");
MethodInfo localMethod = typeof(LocalInterface_Event).GetMethod("add_WindowActivate");

Assert.IsTrue(interopMethod.IsSpecialName);
Assert.IsTrue(localMethod.IsSpecialName);
Run Code Online (Sandbox Code Playgroud)

此外,无法创建继承来自interop接口的接口以解决问题,因为它还将继承marshalled add/ remove方法.

此问题已在此处的Moq问题跟踪器中报告:http://code.google.com/p/moq/issues/detail? id = 226

更新:

在Moq开发人员解决这个问题之前,唯一的解决方法可能是使用反射来修改界面,这似乎违背了使用Moq的目的.不幸的是,为这个案例"推出自己的"Moq可能更好.

此问题已在Moq 4.0(2011年8月发布)中修复.