7

We have a home-made COM component written in C++. We now want to test its functions and events in a C# Test Project. The function tests are pretty straight-forward. However, the events are never triggered.

MyLib.MyClass m = new MyLib.MyClass();
Assert.IsTrue(m.doStuff()); // Works

// This does not work. OnMyEvent is never called!
m.MyEvent += new MyLib.IMyClassEvents_MyEventHandler(OnMyEvent);
m.triggerEvent();

I've googled this and read about similar issues here on StackOverflow. I've tried all proposed methods but can't get it working!

So far I've tried running my test with an active dispatcher but with no success. I also tried manually pumping messages in the main thread using Dispatcher.PushFrame(). Nothing. My events never trigger. I created a simple WinForms project and verified that my events work in a normal setup. Hence, this issue only applies to Unit Tests.

Q: How do I make a regular C# Unit Test that can successfully trigger active event handlers?

Somebody out there ought to have a working sample! Please help.

l33t
  • 18,692
  • 16
  • 103
  • 180
  • Well, unit test failed. COM servers tend to need the program to pump a message loop before they can generate events. It is part of the STA contract. Contact the component author for support. – Hans Passant Dec 14 '11 at 13:53
  • The COM server is our own component - which we want to test. Pumping messages will, as you say, be essential. So the question remains; how do you accomplish this in a unit test? – l33t Dec 14 '11 at 14:11
  • @NOPslider What unit testing framework are you using? Later versions of NUnit default to an MTA threading model. – vcsjones Dec 30 '11 at 02:48
  • Have you tried to run this in a Windows Forms application / environment? At least to check if it's any better about the event? – Simon Mourier Dec 31 '11 at 07:07
  • Could you post your actual unit test code? It seems the COM component is not working properly (since an STA component should still work from an MTA context - *if* it is registered correctly). And the active dispatcher approach should work as well, creating a separate STA thread. – Stephen Cleary Dec 31 '11 at 23:35
  • @SimonMourier Yes, it works in a Windows Forms application. The problem here is that regular unit tests don't have a message pump. – l33t Jan 01 '12 at 19:04
  • @NOPslider - ok, have you tried to call Application.DoEvents "sometimes" in your code? You *need* a message pump somewhere. – Simon Mourier Jan 02 '12 at 08:04
  • @SimonMourier `Dispatcher.PushFrame` is the rough WPF equivalent of `DoEvents` from WinForms. – vcsjones Jan 03 '12 at 01:26
  • Stephen Cleary is actually right, it should work. I have tested a fresh VS 2010 COM component written in C++ (with connection points and all that jazz) in a C# Console app without any explicit message pump, and it works, event when switching the C# thread between 'STA' and 'MTA' (implicit), even when switching the component threading model between 'Apartment' (implicit) and 'Both'. You may have a problem with the MyEvent implementation. – Simon Mourier Jan 04 '12 at 09:13
  • Under what circumstances is the event supposed to be fired? Try sticking a MessageBox() in right there (in the C++ code, just before you raise the event) and see if it pops up. If not, it isn't being fired at all. Until you get past that hurdle there is no point asking why the C# code doesn't see the event. – Ben Jan 04 '12 at 15:39
  • @HansPassant - not all COM servers need a message pump to fire an event - that's only STA ActiveX controls –  Apr 13 '12 at 01:57
  • @Micky - It has nothing to do with ActiveX. – Ritch Melton Apr 13 '12 at 02:01
  • 2
    Unit testing COM? You poor devil. Technically though, I think you would call that integration testing. – Damian Powell Jun 10 '12 at 09:44

1 Answers1

1

If your COM object is an STA object, you probably need to run a message loop in order to make its events fire.

You can use a small wrapping around the Application and Form object to do that. Here is a small example I wrote in a few minutes.

Note that I did not run or test it, so it may not work, and the cleanup should probably be better. But it may give you a direction for a solution.

Using this approach, the test class would look something like this:

[TestMethod]
public void Test()
{
    MessageLoopTestRunner.Run(

        // the logic of the test that should run on top of a message loop
        runner =>
        {
            var myObject = new ComObject();

            myObject.MyEvent += (source, args) =>
            {
                Assert.AreEqual(5, args.Value);

                // tell the runner we don't need the message loop anymore
                runner.Finish();
            };

            myObject.TriggerEvent(5);
        },

        // timeout to terminate message loop if test doesn't finish
        TimeSpan.FromSeconds(3));
}

And the code for the MessageLoopTestRunner would be something like that:

public interface IMessageLoopTestRunner
{
    void Finish();
}

public class MessageLoopTestRunner : Form, IMessageLoopTestRunner
{
    public static void Run(Action<IMessageLoopTestRunner> test, TimeSpan timeout)
    {
        Application.Run(new MessageLoopTestRunner(test, timeout));
    }

    private readonly Action<IMessageLoopTestRunner> test;
    private readonly Timer timeoutTimer;

    private MessageLoopTestRunner(Action<IMessageLoopTestRunner> test, TimeSpan timeout)
    {
        this.test = test;
        this.timeoutTimer = new Timer
        {
            Interval = (int)timeout.TotalMilliseconds, 
            Enabled = true
        };

        this.timeoutTimer.Tick += delegate { this.Timeout(); };
    }

    protected override void OnLoad(EventArgs e)
    {
        base.OnLoad(e);

        // queue execution of the test on the message queue
        this.BeginInvoke(new MethodInvoker(() => this.test(this)));
    }

    private void Timeout()
    {
        this.Finish();
        throw new Exception("Test timed out.");
    }

    public void Finish()
    {
        this.timeoutTimer.Dispose();
        this.Close();
    }
}

Does that help?

Ran
  • 5,989
  • 1
  • 24
  • 26