在单元(集成)测试中没有从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 />
部分,然后
忽略最后的更新和分辨率。它们包含误导和误报,以及在我工作的整个COM、Regfree COM、Interop和COM Events世界中对我的代表缺乏理解。问题仍未解决。
关键问题仍然是,当COM在单元测试的上下文中运行时,COM事件不会触发。当在普通的。exe中运行时,它们工作得很好
我的猜测,不知道究竟是什么非托管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();
});
}