我有一个非托管DLL
,我试图创建一个.NET
包装器库,但是当我尝试对它进行NUnit(v3)
测试时,我会遇到不同的行为如果它只是从一个按钮运行单击WinForm应用程序。
背景:在DLL启动期间,我调用它的Connect()
方法,最终导致DLL建立TCP连接。当建立TCP连接时,我会通过将处理程序连接到其连接的"已连接"事件。
连接后,我再调用DLL上的其他命令。
在一个简单的Winforms应用程序测试中,我有一个实例化" 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
UPDATE1: 如果使用MSTest它的工作原理!所以它与NUnit的创业环境有关。
UPDATE2: 通过this SO帖子中的调查,我偶然地复制了相同的行为,没有任何单元测试框架,而是通过reg免费COM。所以我现在认为它与COM如何被激活和消耗有关?
解决
终于找到了答案。
感谢Chris对this问题的回答。我所要做的就是在概述中添加一个<comInterfaceExternalProxyStub />
部分和bingo
更新4
忽略上次更新和解决方案。它们包含误导和误报,以及我在COM,Regfree COM,Interop和COM Events的整个世界中工作时缺乏对我的理解。问题仍未解决。
关键问题仍然是当COM在单元测试的上下文中运行时,COM事件不会触发。当在普通的.exe中运行时,它们可以正常工作
答案 0 :(得分:0)
我的猜测是,在不知道非托管DLL究竟在做什么的情况下,它是一个单线程单元(STA)COM dll。在这个线程模型中,COM interop会将对DLL的所有调用编组到创建对象的线程中(在单元测试中阻塞等待自动重置事件,因此没有任何反应)。
事件模式在Winforms应用程序中有效,因为主UI线程是STA线程(检查主方法上的属性)并且它正在传递消息,因此允许回调并且锁定被COM消息泵送取代。
如果是这种情况,测试包装器的唯一方法是创建一个STA线程,在其上运行消息泵,然后将消息传递给线程以触发创建COM对象和连接(换句话说,它是一个巨大的痛苦)。更糟糕的是,对象在客户端应用程序中也会以这种方式运行,因此除非您在包装器中创建一个STA线程并对所有调用进行编组,否则您将无法异步使用它。
答案 1 :(得分:0)
正如Chris所提到的,这是因为特定于在STA线程中使用COM Interop对象。发生这种情况是因为只能从该线程访问STA线程中创建的互操作对象(也称为事件调用)。 您需要做的就是将任何COM Interop的创建包装在单独的线程中。 像这样的东西:
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();
});
}