Sunday, September 21, 2008

Run unit test on MTA thread. VSTS test runner helper.

How much trouble is required to make you invent your own wheel? For me it was first vstesthost exiting on the unhandled exception in the worker thread and then VS running tests by default on the STA thread finally broke my back and I decided to put a small helper together to rectify all the above troubles.

Why to run on the MTA thread? Sometimes you use functions like WaitHandle.WaitAll that do require to be executed on the MTA thread. The exception you get is "WaitAll for multiple handles on a STA thread is not supported". You might as well have to use MTA because of specific COM needs.

You can setup the local test run configuration to run tests on the MTA thread as described here: http://blogs.msdn.com/ploeh/archive/2007/10/21/RunningMSTestInAnMTA.aspx
Although the limitation of this solution is that it is global. In MbUnit you can setup the apartment as a property of a TextFixture attribute, that again I believe may not be granular enough.

I want to be able to run a separate call as MTA or STA really, because doing it can actually be the thing under scope.

Decided so, surely we need to resolve unhandled exceptions code under test may throw that would lead to the unpleasant death of our test host ("VSTestHost.exe has encountered a problem and needs to close.  We are sorry for the inconvenience."). Whatever MS says about this being by design (http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=92232&SiteID=1), this is a bad example of a blind application of the same design decision to all possible contexts (although safe default is required, handling failures like this always require flexible and context aware solutions).

That is why I put together a helper class (the most recent version available at http://code.google.com/p/toolsdotnet/source/browse/trunk/Tools.Net/src/Tools.Tests.Helpers/TestRunner.cs):

using System;
using System.Threading;
 
namespace Tools.Tests.Helpers
{
    public class TestRunner
    {
        private Action action;
        private ApartmentState apartmentState;
        private Exception exception;
 
        public TestRunner(Action action, ApartmentState apartmentState)
        {
            this.action = action;
            this.apartmentState = apartmentState;
        }
        public void Execute()
        {
            // Setup a worker thread
            Thread workerThread = new Thread(new ThreadStart(ExecuteInternal));
            // Set apartment
            workerThread.SetApartmentState(apartmentState);
 
            workerThread.Start();
            // Wait until work on the worker thread is done
            workerThread.Join();
            // Probe for unhandled exception
            if (exception != null)
            {
                // If exception is present, rethrow here on the main thread
                throw exception;
            }
        }
        private void ExecuteInternal()
        {
            // wrap our original action in the try/catch
            try
            {
                action();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
                // Don't consider race to happen here, subject to think more
                exception = ex;
                // Don't rethrow here as that would kill the test host
            }
        }
    }
}
The usage would be (sorry for such a big sample, don't have time to make it shorter now :):
            var consumerManager = new ConsumerManager();
 
            var target = new ConsumerManager_Accessor(new PrivateObject(consumerManager));
 
            new TestRunner(() =>
            CompositePatternTestHelper.TestForCompositeOperation<ConsumerManager_Accessor, IProcess, TrivialAsyncResultMock>
                (
                target, parent => parent.Stop(), child => { child.BeginStop(null, new AsyncCallback(target.ConsumerStoppedCallback)); return new TrivialAsyncResultMock(); }, (parent, child) =>
                    parent.Consumers.Add(child)
                    ),
                    ApartmentState.MTA).Execute();
 
            Assert.AreEqual(ProcessExecutionState.Stopped, consumerManager.ExecutionState);

And it runs ok, if I change the above sample code to run under STA (ApartmentState.STA).Execute();) I'm getting:

image

Which is expected.

Post a Comment