Unit testing async Windows Phone 8 code on the UI thread with VS 2012.2 CTP4
This may be the most cryptic acronym-laden title I ever used for a blog post, but it quite exactly describes what I was trying to do yesterday.
The Visual Studio 2012 CTP4 makes it possible to write real Windows Phone 8 unit tests that run in the Visual Studio Unit Test runner (in stead of only on the emulator). So when I wanted to investigate the Routing API that is new in Windows Phone 8, I decided not to write an application outright, but start out with unit test.
I set up a new solution with two projects, as I usually do: one with the actual app - and one class library with the view models, models and other logic in it that isnot directly related to the user interface. And then I added a Windows Phone 8 Unit Test App.
First things first: when I want to test routing, I first need to give the user an option to select a location to go to. I decided to use the Geocoding API. I decided the view model should contain the following:
- A string property SearchText to be filled by the user
- An ObservableCollection of MapLocation called MapLocations to be filled by the Geocoder, intended to be bound to a list control of some kind to enable the user to select on of the founds locations.
- A MapLocation property SelectedLocation to hold the MapLocation selected by the user
- A little method to actually perform the geocoding
- A command wrapping this method.
My good and very smart friend - and fellow Phone development MVP - Matteo Pagani has already covered some ground in this direction by writing this article and inspired by it I decided to pull in the Microsoft.Bcl.Async library as well so I could use async/await, on the premises that you can never have too much beta software in your project ;-)
The method I wanted to test was pretty simple:
public async Task SearchLocation() { MapLocations.Clear(); SelectedLocation = null; var geoCoder = new GeocodeQuery {
SearchTerm = SearchText, GeoCoordinate = new GeoCoordinate() }; MapLocations.AddRange(await geoCoder.GetMapLocationsAsync()); }
And so was the test method – I let it search for the street I live in.
[TestMethod] public async Task TestLocationWrong1() { var testVm = new GeocodeViewModel {SearchText = "Springerstraat Amersfoort Netherlands"}; await testVm.SearchLocation(); Assert.IsTrue(testVm.MapLocations.Any()); }
I ran the test…. and was quite surprised by the result. “Invalid cross thread access"??? I don’t even have a UI. Very interesting. Apparently the GeocodeQuery needs to be run on the UI thread. As to why this is, I have no idea. Some people (hi Morten ;-) ) say that if you have to unit test on the UI thread, you are doing it wrong. That may be the case, but it seems I have little choice here and I still want to test my view model.
According to this page there is a UITestMethodAttribute for Windows Store applications to solve this kind of problems – but not for Windows Phone 8 (yet) so obviously I had to pull in the Dispatcher. Since calling stuff from the Dispatcher runs asynchronously as well take 2 didn’t work of course…
[TestMethod] public void TestLocationWrong2() { var testVm = new GeocodeViewModel { SearchText = "Springerstraat Amersfoort Netherlands" }; Deployment.Current.Dispatcher.BeginInvoke(async () => await testVm.SearchLocation()); Assert.IsTrue(testVm.MapLocations.Any()); }
…for the simple reason that the although testVM.SearchLocation is now fired on the UI thread, the Assert is not, and it is executed directly after the BeginInvoke is called and MapLocations still is empty when the Assert is evaluated.
I don’t know if there’s a smarter way to do this, but I used an AutoResetEvent to solve it. I used that to block the test thread until the UI thread is done, like this:
[TestMethod] public void TestLocationSearchHasResult() { var waitHandle = new AutoResetEvent(false); var testVm = new GeocodeViewModel { SearchText = "Springerstraat Amersfoort Netherlands" }; Deployment.Current.Dispatcher.BeginInvoke(async () => { await testVm.SearchLocation(); waitHandle.Set(); }); waitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.IsTrue(testVm.MapLocations.Any()); }
The test thread waits until waitHandle.Set() is called – or five seconds, whatever happens first – and then it performs the Assert. And that works.
As usual, you can download a demo solution here. It was actually meant to be a solution demoing the Route API, as stated earlier, but I thought this subject deserved a blog post on its own.
As stated, this project requires installation of the Visual Studio 2012 CTP4. This has a GoLive license, but it’s still preview software. Install it on your own risk.
Update: Pedro Lamas, a Windows Phone Development specialist working for Nokia, has posted about his port of UITestMethodAttribute to Windows Phone. That runs the whole test on the UI thread in stead of only the the mandatory part. This brute-force method may not be desirable for all cases, but it sure is pretty easy to use.