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

时间:2010-01-19 21:25:13

标签: c# unit-testing nunit mocking moq

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

这个第三方组件实现了几个接口,但还需要使用事件回调到我们的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对象(通过互操作程序集)交谈的事实有关,但我不确定如何解决问题。

2 个答案:

答案 0 :(得分:14)

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

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

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

var mock = new Mock<IInterfaceWithEvent>;
mock.Raise(e => e.MyEvent += null);

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

Moq使用反射作为从名称add_开始并设置了IsSpecialName标志的方法检测事件处理程序方法。这项额外检查是过滤掉与事件无关但名称以add_开头的方法调用。

在该示例中,截获的方法将被称为add_MyEvent,并且将设置IsSpecialName标记。

但是,对于interop中定义的接口来说似乎并不完全正确,因为尽管事件处理程序方法的名称以add_开头,但它的{{1}标志集。这可能是因为事件正在通过较低级别的代码编组到(COM)函数,而不是真正的“特殊”C#事件。

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

IsSpecialName

此外,无法创建继承来自interop接口的接口以解决问题,因为它还将继承已编组的MethodInfo interopMethod = typeof(ApplicationEvents4_Event).GetMethod("add_WindowActivate"); MethodInfo localMethod = typeof(LocalInterface_Event).GetMethod("add_WindowActivate"); Assert.IsTrue(interopMethod.IsSpecialName); Assert.IsTrue(localMethod.IsSpecialName); / add方法。

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

<强>更新

在Moq开发人员解决这个问题之前,唯一的解决方法可能是使用反射来修改界面,这似乎会破坏使用Moq的目的。不幸的是,为这种情况“推出自己的”Moq可能更好。

此问题已在Moq 4。0(2011年8月发布)中修复。

答案 1 :(得分:-1)

您可以从第三方重新定义COM接口并将其与moq一起使用。

看起来你的目的是消除外部依赖,并且moq不能很好地与COMInterop程序集一起使用,你应该能够打开反射器并从interop程序集中提取你想要的任何接口定义,定义模拟并运行你的单元测试