【问题标题】:Events not firing from COM component when in Unit (Integration) Test在单元(集成)测试中未从 COM 组件触发事件
【发布时间】:2016-10-09 11:48:10
【问题描述】:

我有一个非托管的DLL,我正在尝试为其创建一个.NET 包装库,但是当我尝试对其运行NUnit(v3) 测试时,与它只是从按钮单击一个 WinForm 应用程序。

背景:在 DLL 启动期间,我调用了它的Connect() 方法,最终导致 DLL 建立 TCP 连接。当 TCP 连接建立后,我会通过将处理程序连接到其“已连接”事件来获得通知。 连接后,我会在 DLL 上调用其他命令。

在一个简单的测试 Winforms 应用程序中,我有 1 个按钮来实例化“DLL”,然后调用 Connect() 方法。当线程完成时,应用程序会闲置大约 2 秒,然后“已连接”事件处理程序按预期触发。该事件不返回任何内容。

但是因为connect() 是一项昂贵的操作,而且因为我的库注定要用于更大的应用程序,所以我在我的库中创建了一个ConnectAsync() 方法并使用了async and await 关键字和AutoResetEvent . ConnectAsync() 方法在收到通知 TCP 连接已从事件启动后返回“实例化”DLL 的实例。 对测试 WinForms 应用程序进行了一些重构,它按预期工作。

下一步是使用 NUnit 进行集成测试。但是,当从异步测试调用 ConnectAsync() 方法时,我可以看到远程应用程序上建立了 TCP 连接,但事件处理程序永远不会触发。一天的测试、搜索和反复试验无法解释为什么 ConnectAsync() 在简单的 Winforms 按钮上完美运行,但在 UnitTest 中却不行。

这是测试代码

[Test()]
public async Task Test1()
{
    var conn = await GetConnection();
    //assert some commands on the conn
}

private async Task<TCPConnector> GetConnection()
{   
    return await Task.Run(() =>
    {
        var mre = new AutoResetEvent(false);        
        var ctrl = new TCPConnector();
        ctrl.serverName = server;
        ctrl.serverPort = serverPort;
        ctrl.onConnected += () => { mre.Set(); };
        ctrl.Connect();
        mre.WaitOne();
        return ctrl;
    });
}

我知道这不是一个严格的问题,但我很困惑,正在寻找尝试的想法。或者是关于按钮单击事件和 NUnit 测试执行之间有什么不同的指针。

如果它对某人意味着什么,我调用的 dll 是一个非托管的 ActveX

更新1: 如果使用 MSTest 它可以工作!所以跟NUnit的启动环境有关。

更新 2: 通过this SO 帖子中的调查,我偶然在没有任何单元测试框架的情况下复制了相同的行为,而是通过 reg free COM。所以我现在认为这与 COM 的激活和消耗方式有关?

分辨率 终于找到了答案。 感谢 Chris 对this 问题的回答。我所要做的就是按照概述在清单中添加一个&lt;comInterfaceExternalProxyStub /&gt; 部分,然后宾果游戏

更新 4

忽略最近的更新和解决方案。它们包含误导和误报,以及当我在整个 COM、Regfree COM、Interop 和 COM 事件的世界中工作时代表我缺乏理解。问题仍未解决。

关键问题仍然是,当 COM 在单元测试的上下文中运行时,COM 事件不会触发。在普通的 .exe 中运行时,它们工作得很好

【问题讨论】:

  • 提供更多关于测试代码的细节
  • @OrdinaryOrange:我会尝试 using TaskCompletionSource 而不是 Task.RunAutoResetEvent
  • 感谢斯蒂芬的建议。我按照建议重构了代码以使用TaskCompletionSource,但得到的结果与以前相同。我想这一定是启动环境之间的一些差异。

标签: c# .net winforms nunit unmanaged


【解决方案1】:

我的猜测是,在不知道非托管 DLL 到底在做什么的情况下,它是一个单线程单元 (STA) COM dll。在此线程模型中,COM 互操作将对 DLL 的所有调用编组到创建对象的线程(在您的单元测试中,该线程在等待自动重置事件时被阻塞,因此没有任何反应)。

事件模式在 Winforms 应用程序中有效,因为主 UI 线程是一个 STA 线程(检查您的 main 方法的属性)并且它正在泵送消息,因此允许回调并且锁被 COM 消息泵送所取代。

如果是这种情况,测试包装器的唯一方法是创建一个 STA 线程,在其上运行消息泵,然后将消息传递给线程以触发 COM 对象和连接的创建(换句话说,这是一个巨大的痛苦)。更糟糕的是,该对象在客户端应用程序中也会以这种方式运行,因此除非您在包装器中创建一个 STA 线程并编组对它的所有调用,否则您将无法异步使用它。

【讨论】:

  • 哈,我只是在测试这个,然后回到这里也看到了你的帖子。所以我可以直接回答。不幸的是没有运气。我将调用包装在 STA 线程中(使用来自 this question 的方法),但这没有任何效果。我确实发现,如果我在 MSTest 中运行它,它可以完美运行。所以这和NUnit有关。
  • 是的,甚至将Connect() 放在Application.Idle 事件中以确保。 Connect() 会触发,我可以看到远程端的连接,但是onConnect 事件仍然不会触发。
【解决方案2】:

正如 Chris 所提到的,这是因为在 STA 线程中使用 COM 互操作对象是特定的。发生这种情况是因为只能从该线程访问(也是事件调用)在 STA 线程中创建的互操作对象。 您所需要的只是将任何 COM 互操作的创建包装在一个单独的线程中。 类似的东西:

private async Task<TCPConnector> GetConnection()
{   
    return await Task.Run(() =>
        {
            var mre = new AutoResetEvent(false);        
            Create(mre);
            mre.WaitOne();
            return ctrl;
        });
}

private TCPConnector ctrl;
private void Create(AutoResetEvent mre) 
{    
    
    ThreadPool.QueueUserWorkItem(o =>
    {
        ctrl = new TCPConnector();
        ctrl.serverName = server;
        ctrl.serverPort = serverPort;
        ctrl.onConnected += () => { mre.Set(); };
        ctrl.Connect();
    });
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-08
    • 2010-12-26
    • 1970-01-01
    相关资源
    最近更新 更多