在单元(集成)测试中没有从COM组件触发事件

本文关键字:COM 组件 事件 单元 集成 测试 | 更新日期: 2023-09-27 18:12:17

我有一个未管理的DLL,我试图创建一个.NET包装库,但当我尝试运行NUnit(v3)测试时,与它相比,如果它只是从一个按钮点击WinForm应用程序运行。

后台:在启动DLL时,我调用它的Connect()方法,最终导致DLL建立TCP连接。当TCP连接建立后,通过将处理程序连接到其"Connected"事件,我得到通知。一旦连接,我就调用DLL上的其他命令。

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

但是因为connect()是一个昂贵的操作,而且因为我的库是为更大的应用程序设计的,所以我在库中创建了一个ConnectAsync()方法,并使用了async和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是一个非托管的activex

Update1: 如果使用mest,它可以工作!这和NUnit的启动环境有关

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

终于找到了答案。感谢克里斯对这个问题的回答。我所要做的就是在清单中添加一个<comInterfaceExternalProxyStub />部分,然后

更新4

忽略最后的更新和分辨率。它们包含误导和误报,以及在我工作的整个COM、Regfree COM、Interop和COM Events世界中对我的代表缺乏理解。问题仍未解决。

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

在单元(集成)测试中没有从COM组件触发事件

我的猜测,不知道究竟是什么非托管DLL正在做的,是它是一个单线程公寓(STA) COM DLL。在这个线程模型中,COM互操作将把所有对DLL的调用编组到创建对象的线程(在你的单元测试中,它被阻塞等待自动重置事件,因此什么也没有发生)。

事件模式在Winforms应用程序中工作,因为主UI线程是一个STA线程(检查主方法的属性),它正在抽取消息,所以允许回调,锁被COM消息抽取取代。

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

正如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();
    });
}