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

7
我们一直在尝试编写C#的worker类的单元测试,使用moq模仿一个基于COM的第三方API来动态创建模拟对象。我们使用NUnit作为单元测试框架。
这个第三方组件实现了一些接口,但也需要通过事件回调到我们的worker类中。我们的计划是模拟这个第三方组件可能引发的事件,并测试我们的worker类是否按预期操作。
不幸的是,我们遇到了一个问题,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);
        }
    }
}

有人能解释一下为什么对于本地定义的接口可以触发事件,但是导入的第三方API(在这种情况下是Word)却不行吗?

我有一种感觉,这可能与我们通过互操作程序集与COM对象进行通信有关,但我不确定如何解决这个问题。


1
看起来这个 bug 已经在 Moq v4.0 中被修复了:http://code.google.com/p/moq/issues/detail?id=226 - g t
2个回答

14

Moq通过检测对事件内部方法的调用来“拦截”事件。这些方法的名称为add_+事件名称,并且是非标准C#方法,具有“特殊”的属性。事件有点像属性(get/set),可以按以下方式定义:

event EventHandler MyEvent
{
    add { /* add event code */ };
    remove { /* remove event code */ };
}

如果要定义一个接口用于Moq的上述事件,则可使用以下代码触发该事件:

var mock = new Mock<IInterfaceWithEvent>;
mock.Raise(e => e.MyEvent += null);
由于在C#中无法直接引用事件,因此Moq拦截模拟对象上的所有方法调用并测试是否调用添加事件处理程序的方法(在这种情况下,将添加一个空处理程序)。如果是这样,就可以间接地通过该方法的“目标”获得引用。
Moq使用反射检测事件处理程序方法,方法名称以add_开头,并设置了IsSpecialName标志。这个额外的检查是为了过滤掉与事件无关但名称以add_开头的方法调用。
在这个例子中,拦截的方法将被称为add_MyEvent,并且将设置IsSpecialName标志。
但是,在交互定义的接口中似乎并非完全如此,因为虽然事件处理程序方法的名称以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);
此外,无法创建继承interop接口的接口来解决此问题,因为它也会继承已封送的add/remove方法。

此问题在Moq问题跟踪器上报告如下:http://code.google.com/p/moq/issues/detail?id=226

更新:

在Moq开发人员解决此问题之前,唯一的解决方法可能是使用反射修改接口,这似乎违背了使用Moq的目的。不幸的是,在这种情况下,最好只是自己编写'Moq'。

此问题已在Moq 4.0中得到修复(发布于2011年8月)。


谢谢 - 我们会关注moq的错误报告。我们最终只是编写了一个简单的包装类来封装第三方API,然后我们可以使用单元测试进行模拟。 - John Sibly
谢谢你找到了这个问题。我也遇到了同样的问题,只不过是在F#中定义的接口。结果发现它们也省略了IsSpecialName标志并且也面对着这个问题。 - JaredPar

-1

您能否重新定义第三方的COM接口并使用moq进行操作?

看起来您的意图是通过moq消除外部依赖,但是moq与COMInterop程序集不兼容。您可以打开反编译工具Reflector,并从interop程序集中提取任何接口定义,定义模拟对象并运行单元测试。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接