Sharing An IE Instance Among Multiple Tests With WatiN And MS Test

4 commentsWritten on March 24th, 2011 by
Categories: testing

Lost some time yesterday trying to get something working with MS Test (not my choice, but that's what my client uses) that i had expected to be easy. After all, it was especially easy to get working with NUnit. I wanted to create a base testing fixture which would instantiate one instance of Internet Explorer for the entire test run, and make that instance available to each test in the assembly. Sounds easy, no?

First problem: MS Test runs each test on a different thread.

When you use IE through WatiN, it uses COM behind the scenes. Accessing COM objects from different threads is not a safe thing to do and can lead to the following exception: System.Runtime.InteropServices.InvalidComObjectException: COM object that has been separated from its underlying RCW cannot be used.

Running each test individually worked, but running the entire suit made every test except for the first one fail with that exception because MS Test uses a different thread for each test (i suppose the development team did that to make sure it was enterprisey). Quite annoying, but luckily for me, the only other guy in the world who uses MS Test with WatiN also ran into the same problem and he described his workaround on his blog.

I made minor modifications to his IEStaticInstanceHelper class (basically just turned it into a static class) so my version looks like this:

    public static class IEStaticInstanceHelper
    {
        // TODO: move this to a config file
        public const string ROOT_URL = "http://localhost:13834/";

        private static IE _ie;
        private static int _previouslyKnownIeThreadHashCode;
        private static string _ieHwnd;

        public static void Initialize()
        {
            IE = new IE(ROOT_URL);
        }

        public static IE IE
        {
            get
            {
                if (GetCurrentThreadHashCode() != _previouslyKnownIeThreadHashCode)
                {
                    _ie = Browser.AttachTo<IE>(Find.By("hwnd", _ieHwnd));
                    _previouslyKnownIeThreadHashCode = GetCurrentThreadHashCode();
                }
                return _ie;
            }
            private set
            {
                _ie = value;
                _ieHwnd = _ie.hWnd.ToString();
                _previouslyKnownIeThreadHashCode = GetCurrentThreadHashCode();
            }
        }

        private static int GetCurrentThreadHashCode()
        {
            return Thread.CurrentThread.GetHashCode();
        }
    }

I also had the following AssemblyInitialize and AssemblyCleanup methods:

        [AssemblyInitialize]
        public static void AssemblyInitialize(TestContext testContext)
        {
            IEStaticInstanceHelper.Initialize();
        }

        [AssemblyCleanup]
        public static void AssemblyCleanup()
        {
            if (IEStaticInstanceHelper.IE != null)
            {
                IEStaticInstanceHelper.IE.Close();
                IEStaticInstanceHelper.IE.Dispose();
            }
        }

MS Test will call the AssemblyInitialize method before any test in the assembly is executed, provided that you don't forget to add the TestContext parameter to your method or it will silently be ignored (WTF?!). It'll also call the AssemblyCleanup method after each test in the assembly has finished executing.

Second problem: MS Test runs the AssemblyCleanup method in an MTA thread, even though each test is executed in STA threads by default.

As you can see in my AssemblyCleanup method, i access the IE property of IEStaticInstanceHelper. That property getter contains the following line:

    _ie = Browser.AttachTo<IE>(Find.By("hwnd", _ieHwnd));

That line works perfectly during the execution of the tests. When it is called from the AssemblyCleanup method, it times out after 30 seconds because it can't seem to find the IE window with the handle (_ieHwnd) that is known to be valid. And this, apparently, is because the current thread is an MTA thread when we're within the AssemblyCleanup method instead of an STA thread. I can't for the life of me figure out why they'd use an MTA thread for the AssemblyCleanup method while they use STA threads for the tests, but i will again assume it was done to keep up to the high enterprisey standard that people expect from something like MS Test.

The solution, while a horrible hack, is quite simple and works perfectly:

        [AssemblyCleanup]
        public static void AssemblyCleanup()
        {
            var thread = new Thread(() =>
            {
                if (IEStaticInstanceHelper.IE != null)
                {
                    IEStaticInstanceHelper.IE.Close();
                    IEStaticInstanceHelper.IE.Dispose();
                }
            });

            thread.SetApartmentState(ApartmentState.STA);
            thread.Start();
            thread.Join();
        }

There... nice and enterprisey.

  • http://twitter.com/rebootd pirate josh

    cool. if I’m ever forced to use mstest (may I be strung up by my toes first), I’ll know where get this work around.

  • Lotte

    Could you not simply add the STAThreadAttribute ?

    • http://davybrion.com Davy Brion

      just tried it, but it doesn’t work… still causes the timeout problem :s

  • sinful bikinis

    Movado Serenade,Movado Faceto,Movado Elliptica,Omega Watches,Omega Replica Watches,Replica Omega Watches,Swiss Omega Watches,Omega constellation,Omega Deville,Omega double eagle,Omega world ocean,Omega railmaster Co-axial,Omega Seamaster Co-axial,Omega Seamaster professional,Omega Speedmaster chronometer,Omega Speedmaster professional,Omega Watches,replica omega watch,swiss omega watch,Omega Double Eagle automated breguet watches for sale Chronograph,Omega Double Eagle Chronometer,Omega Double Eagle Chronometer Mid Size,Omega Co_Axial automated Chronometer,Omega Co_Axial energy Reserve,Omega Constellation Classic,Omega De Ville CoAxial CoAxial Rattrapante,Omega breguet ladies watch Moonphase,Omega Olympic Series,Omega De Ville breguet pocket watch Co Axial Chronograph,Omega De Ville Co Axial Chronometer,Omega De Ville Co Axial GMT,Omega Seamaster 300 M Chronometer,Omega Seamaster 300 M GMT,Omega Seamaster 300 M Racing Chronometer,Omega Seamaster world Ocean,Omega Seamaster world Ocean Chrono,Omega Seamaster Railmaster Chronograph,Omega Seamaster distinctive Edition,Speedmaster Broad Arrow,Omega Speedmaster Professional,Omega Speedmaster Racing Chronometer,Omega Speedmaster Snoopy Edition,Omega Seamaster,Omega Seamaster Aqua Terra,Omega Seamaster Aqua Terra Chronometer,Omega Seamaster Diver Series,Omega Seamaster professional 300m Seamaster 300m GMT,Omega Speedmaster,Omega SpeedMaster Date,Oris Watches,Oris Replica Watches,Replica Oris Watches,Swiss Oris Watches,Oris TT1,Oris cheap Breguet Watches TT3 Chronograph.Panerai Watches,Replica Panerai Watches,Panerai Replica Watches,Swiss Panerai Watches,Panerai day time day Tourbillon,Panerai Ferrari Granturismo GMT,Panerai Ferrari Granturismo breguet mens watch Rattrapante,Panerai Ferrari Scuderia wholesale Breguet Watches GMT,Panerai Luminor,Panerai Luminor Arktos,Panerai Luminor automated Tourbillion,Panerai Luminor Classic,Panerai Luminor Daylight,Panerai Luminor replica Breguet Watches GMT,Panerai Luminor Marina leather-based Band,Panerai breguet classique watches Luminor Marina Rubber Band,Panerai Luminor Marina SS Bracelet,Panerai Luminor energy Reserve,Panerai Luminor Regatta,Panerai Luminor Submersible,Panerai Radiomir,Panerai Radiomir Patek Phellolippe Tourbillion,Panerai Slytech
      var _gaq = _gaq || [];  _gaq.push(['_setAccount', 'UA-13121660-3']);  _gaq.push(['_trackPageview']);
      (function() { breguet tourbillon watches    var ga = document.createElement(‘script’); ga.type = ‘text/javascript’; ga.async = true;    ga.src = (‘https:’ == document.location.protocol ? ‘https://ssl’ : ‘http://www’) + ‘.google-analytics.com/ga.js’;    var s = document.getElementsByTagName(‘script’)[0]; s.parentNode.insertBefore(ga, s);  })();