Hey,
I recently came across a situation whereby I needed to be able to run unit tests on the main thread of my application. I also had the condition that I must be able to use System.Threading.Tasks.Task objects which use continuations to execute code on the main thread once their background processing has been completed. This meant that I could not just directly run the tests on the main thread as I'd prevent the continations from executing.
I just want to describe what I did and link to the git repository as I do have some upstreamable changes. I can split these out into separate patches and submit them if you want, or feel free to just grab the changes you care about and commit them yourself. Please ignore some of the weird class names/namespace names. The project was originally called 'MacUnit' as I was targeting only MonoMac based applications, but i changed it to 'GuiUnit' and didn't rename everything.
I also copied the ConsoleUI test runner directly into the lib so I'd have a single binary which I could reference when writing tests and also use to execute those tests. It seemed like a fairly sane approach to me :)
So my solution to this was to grab the NUnitLite source code and do the following:
* When the test runner starts up, it does the normal loading of tests and all that.
* When 'RunTests' is invoked we use ThreadPool.QueueUserWorkItem to actually execute everything on a background thread. On the main thread we invoke Application.Init () and then Application.Run () to initialise the GUI toolkit and then start a main loop.
* Reflect.Invoke was modified to invoke the MethodInfo using the equivalent of Application.InvokeOnMainThread. This means that every invocation of a test method, setup method or teardown method happens on the UI thread.
* If the return value of a Test, Setup or Teardown method is a System.Threading.Thread.Tasks, we call Task.Wait (). This allows tests written using async/await to work correctly.
So with code:
This is how I try to detect the Gui toolkit at runtime and call the right setup methods to start the main loop:
Xwt and MonoMac are the two toolkits I added support for in V1 as they are the only toolkits I care about for my project. I will be doing the same for WPF shortly as we also require that. 'ExecuteOnXwt' and 'ExecuteOnMonoMac' are the two corresponding methods for starting the main loop in those toolkits.
I had to add support for async setup/teardown methods. I was lazy about it ;) By calling Task.Wait here we prevent the test harness from executing more tests until the current one has completely finished all it's async/awaits. We also ensure that all Assert calls happen under the right context and any exceptions thrown in the test are propagated correctly.
Note: We do kinda lose the stacktrace here. I didn't care too much about that.
I added support for executing async/await based tests which do not block the main loop
I set Environment.CurrentDirectory to the test assembly directory before invoking each test method. Previously this was never set so any test loading files using relative paths would fail.
AsyncInvocationRegion does not work on Mono.