在单元(集成)测试中,事件不会从 COM 组件触发 [英] Events not firing from COM component when in Unit (Integration) Test

查看:68
本文介绍了在单元(集成)测试中,事件不会从 COM 组件触发的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个非托管的 DLL,我正在尝试为它创建一个 .NET 包装器库,但是当我尝试运行 NUnit 时得到不同的行为(v3) 与仅通过单击 WinForm 应用程序的按钮运行相比,对其进行测试.

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

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

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

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

这是测试代码

[测试()]公共异步任务 Test1(){var conn = await GetConnection();//在conn上断言一些命令}私有异步任务获取连接(){return await Task.Run(() =>{var mre = new AutoResetEvent(false);var ctrl = new TCPConnector();ctrl.serverName = 服务器;ctrl.serverPort = 服务器端口;ctrl.onConnected += () =>{ mre.Set();};ctrl.Connect();mre.WaitOne();返回 ctrl;});}

我知道严格来说这不是一个问题,但我很难过,正在寻找可以尝试的想法.或者关于按钮点击事件和 NUnit 测试执行之间有何不同的指针.

如果它对某人有意义,我正在调用的 dll 是一个非托管的 ActveX

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

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

分辨率终于找到了答案.感谢 Chris 对这个问题的回答.我所要做的就是在清单中添加一个 <comInterfaceExternalProxyStub/> 部分,然后宾果游戏

更新 4

忽略最近的更新和分辨率.它们包含误导和误报,以及当我在 COM、Regfree COM、Interop 和 COM 事件的整个世界中工作时对我缺乏理解.问题仍然没有解决.

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

解决方案

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

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

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

I have an unmanaged DLL which I'm trying to create a .NET wrapper library for but am getting different behavior when I try and run a NUnit(v3) test over it compared to if it is just run from a button click off a WinForm app.

Background: During startup of the DLL I call its Connect() method, which ultimately causes the DLL to make a TCP connection. When the TCP connection is established I then get notified by wiring up a handler to its "Connected" event. Once connected I then call other commands on the DLL.

In a simple test Winforms app, I have 1 button which instantiates the "DLL" and then calls the Connect() method. When the thread completes, the app sits idle for about 2 seconds, and then the "connected" event handler fires as expected. The event does not return anything.

But because the connect() is an expensive operation, and because my library is destined for a larger application, I created a ConnectAsync() method in my library and made use of the async and await keywords, and a AutoResetEvent. The ConnectAsync() method returns an instance of the "instantiated" DLL after it gets notified that the TCP connection is up from the event. A bit of refactoring to the test WinForms app, and it works as expected.

Next step was to make an integration test using NUnit. However when the ConnectAsync() method is called from an async test, I can see the TCP connection establish on the remote application, but the event handler never fires. A day's worth of testing, searching and trial and error could not turn up why the ConnectAsync() works perfect off a simple Winforms button but not from a UnitTest.

Here is the test code

[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;
    });
}

I know this is not strictly a question, but I'm stumped and looking for ideas to try. Or pointers as to what is different between a button click event and a NUnit test execution.

In case it means something to somebody, the dll I'm calling is an unmanaged ActveX

Update1: If use MSTest it works! So it has something to do with NUnit's startup environment.

Update2: Through investigations in this SO post, I by chance replicated the same behaviour without any unit testing frameworks, but instead via reg free COM. So I'm now thinking it is something to do with how the COM is activated and consumed ?

Resolution Finally found the answer. Credit to Chris for his answer on this question. All I had to do was add a <comInterfaceExternalProxyStub /> section to the manifest as outlined, and bingo

UPDATE 4

Ignore the last updates and the resolution. They contain misdirection and false positives, and some lack of understanding of my behalf as I worked through the whole world of COM, Regfree COM, Interop, and COM Events. The problem is still unresolved.

The key issue remains that when the COM is run under the context of a unit test the COM events do not fire. When run in a plain .exe they work just fine

解决方案

My guess, without knowing what exactly the unmanaged DLL is doing, is that it is an single threaded apartment (STA) COM dll. In this threading model, COM interop will marshall all calls to the DLL to the thread that creates the object (which in your unit test is blocked waiting on the auto reset event, thus nothing happens).

The event pattern works in the Winforms app because the main UI thread is an STA thread (check the attribute on your main method) and it is pumping messages, so callbacks are allowed and locks are superseded by COM message pumping.

If this is the case, the only way to test the wrapper would be to create an STA thread, run a message pump on it, then pass a message to the thread to trigger the creation of the COM object and the connection (in other words, it's a huge pain). What's worse is that the object will behave this way in a client application as well, so unless you create an STA thread in your wrapper and marshall all calls to it, you will not be able to use it asynchronously.

这篇关于在单元(集成)测试中,事件不会从 COM 组件触发的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆