I will outline how to setup NUnit testing with WinFX 1.0 (Feb CTP) in order to UnitTest an Avalon Window.
What’s wrong
Points of technology that drive the details.
- WPF windows must run on an STA Thread (Single-Apartment) MS Forums
- NUnit (and TestDriven.net, using the NUnit API) run all tests with an MTA Thread. NUnit Developers Wiki: ExceptionsOnThreads
- In order to generate and start an STA thread from within a running MTA, grasshopper, you must warm up your System.Threading fu and investigate Thread() and Thread.SetApartmentState. GeekNoise : Peter Provost
The recipe to success
- Create a new class in your test Assembly to hold the NUnitHelper namespace and class.
using System; using System.Collections.Generic; using System.Reflection; using System.Security.Permissions; using System.Threading; using System.Text; namespace NUnitHelpers { class CrossThreadTestRunner { private ThreadStart userDelegate; private Exception lastException; public void RunSTA(ThreadStart userdelegate) { this.userDelegate = userdelegate; Thread t = new Thread(new ThreadStart(this.MultiThreadedWorker)); t.SetApartmentState(ApartmentState.STA); t.Start(); t.Join(); if (lastException != null) ThrowExceptionPreservingStack(lastException); } public void Run(ThreadStart userdelegate) { this.userDelegate = userdelegate; Thread t = new Thread(new ThreadStart(this.MultiThreadedWorker)); t.Start(); t.Join(); if (lastException != null) ThrowExceptionPreservingStack(lastException); } [ReflectionPermission(SecurityAction.Demand)] private void ThrowExceptionPreservingStack(Exception e) { FieldInfo remoteStackTraceString = typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic); remoteStackTraceString.SetValue(e, e.StackTrace + Environment.NewLine); throw e; } private void MultiThreadedWorker() { try { userDelegate.Invoke(); } catch (Exception e) { lastException = e; } } } }
This is adapted from Peter Provost’s original code. First, I compressed his constructor/method calls into just the method call. This allows me the cleanest calling-code that I can get with minimal effort. Second, I duplicated Run() into RunSTA() in order to test STA threads that are executing the WPF Window.
- Now my calling class (the actual NUnit testing class) can use this new helper to correctly process WPF testing (handling Assert exceptions instead of raising UnhandledException error inside NUnit’s execution path.) :
using System; using System.Collections.Generic; using System.Text; using System.Threading; using System.Windows; using System.Windows.Automation; using System.Windows.Interop; using NUnit.Framework; namespace NunitTest { [TestFixture] public class Class1 { NUnitHelpers.CrossThreadTestRunner runner = new NUnitHelpers.CrossThreadTestRunner(); [Test] public void truthtest() { Assert.IsTrue(true); } [Test] public void falsetest() { Assert.IsTrue(false); } public static void TestAddressbookWorker() { Window ab = new AddressBookLib.Window1(); ab.Show(); ab.Close(); ab = null; Assert.IsTrue(false, "Forced NUNit Failure"); } [Test] public void TestAddressBook() { runner.RunSTA(new ThreadStart(TestAddressbookWorker)); } } }
Here each test method requires a worker that contains the actual testing code. The calling test is simply delegating the worker method into the CrossThreadTestRunner instance. Using
RunSTA()
starts an STA thread and then the WPF window is started happily. - NUnit Output!! (For those unbelieving)
------ Test started: Assembly: NunitTest.dll ------ TestCase 'NunitTest.Class1.falsetest' failed: C:Visual Studio 2005ProjectsWinFXAddressBookNunitTestClass1.cs(26,0): at NunitTest.Class1.falsetest() TestCase 'NunitTest.Class1.TestAddressBook' failed: Forced NUNit Failure C:Visual Studio 2005ProjectsWinFXAddressBookNunitTestClass1.cs(35,0): at NunitTest.Class1.TestAddressbookWorker() C:Visual Studio 2005ProjectsWinFXAddressBookNunitTestCrossThreadTestRunner.cs(55,0): at NUnitHelpers.CrossThreadTestRunner.MultiThreadedWorker() C:Visual Studio 2005ProjectsWinFXAddressBookNunitTestCrossThreadTestRunner.cs(48,0): at NUnitHelpers.CrossThreadTestRunner.ThrowExceptionPreservingStack(Exception e) C:Visual Studio 2005ProjectsWinFXAddressBookNunitTestCrossThreadTestRunner.cs(24,0): at NUnitHelpers.CrossThreadTestRunner.RunSTA(ThreadStart userdelegate) C:Visual Studio 2005ProjectsWinFXAddressBookNunitTestClass1.cs(42,0): at NunitTest.Class1.TestAddressBook() 1 passed, 2 failed, 0 skipped, took 21.25 seconds.