After my post about reverse geocoding I set out to make a little app to demonstrate routing in Windows Phone 8. The demo app went quite out of hand, so I decided to split the post up in a few smaller posts. In the course of it I am going to build a basic navigation app that enables the user to determine two locations, find a route between those locations, and display them on a map – all using MVVMLight, of course.
And now the 2012.2 update to Visual Studio is released, we can finally build Windows Phone MVVM apps the way things are intended to be: by writing some unit test first, getting the basic functions right, before creating an all-out app. This makes it especially handy to test one important requirement that go for all my apps – all the models and viewmodels must be serializabe, so I can tombstone using SilverlightSerializer like I have been doing for over two years now.
At this point I am not really sure how much blog posts this will take me, but I guess at least three, maybe four.
What is unit testing and why should I do that?
Professional software developers are usually all in on this. What you basically do is write code that asserts that pieces of your code are behaving the way you expect them to do. I am sure everyone has had the episode that you change one little thing that should be inconsequential and suddenly, at some seemingly totally unrelated place, things start going South. Unit tests call little pieces of of your code and test if the result of calling a method, setting a property or whatever gives the result you expect. If you write unit tests, and then change something, and test start failing in unrelated places – it’s like a smoke detector going off. Your code starts detecting bugs for you. Nice, eh? I also gives you the a way to mess around with all kinds of APIs getting things right before you start wasting time on a complex GUI that you can’t get to work because the underlying code cannot work the way you want.
What is geocoding?
Geocoding is what we GIS buffs say when we mean ‘finding a location on earth by it’s name”. If I put “Boston USA” in a geocoder I expect to get a coordinate that puts me somewhere on the east coast of the United States, if I enter “Springerstraat 36 Netherlands” I expect a coordinate that shows me my own house, or somewhere nearby. Some geocoders can take info that’s not tied to an address, but things like, like ‘town hall Little Rock USA”. In general – in goes a descriptive text, out come one or more matches with coordinates.
Enough introduction. Let’s code.
Setting the stage
I started out doing the following:
- Create a new Windows Phone App “NavigationDemo”. Target framework 8.0
- Add a Windows Phone Class Library “NavigationDemo.Logic”
- Add a Windows Phone Unit Test app “NavigationDemo.Logic.Test”
- In NavigationDemo, create a reference to NavigationDemo.Logic
- In NavigationDemo.Logic.Test, make a reference to NavigationDemo.Logic as well.
- In both NavigationDemo and NavigationDemo.Logic.Test, select WMAppManifest.xml in Properties and enable the “ID_CAP_MAP” capbility
Now, because I am a lazy ******* and like to re-use I did things before, bring in the following nuget packages:
- wp7nl (this will pull in MVVMLight Libraries-only version and the Windows Phone toolkit as well)
- Microsoft.Bcl.Async
wp7nl also has a Windows Phone 8 version (it’s name is retained for historic reasons). Install both packages in all three projects.
GeocodeModel – take one
In “NavigationDemo.Logic”, add a folder “GeocodeModel” and put the following class in there:
using System; using System.Collections.Generic; using System.Device.Location; using System.Linq; using System.Threading.Tasks; using Microsoft.Phone.Maps.Services; using Wp7nl.Utilities; namespace NavigationDemo.Logic.Models { public class GeocodeModel { public GeocodeModel() { MapLocations = new List<MapLocation>(); SearchLocation = new GeoCoordinate(); } public string SearchText { get; set; } public GeoCoordinate SearchLocation { get; set; } public MapLocation SelectedLocation { get; set; } public List<MapLocation> MapLocations { get; set; } public async Task SearchLocations() { MapLocations.Clear(); SelectedLocation = null; var geoCoder = new GeocodeQuery { SearchTerm = SearchText, GeoCoordinate = SearchLocation }; MapLocations.AddRange(await geoCoder.GetMapLocationsAsync()); SelectedLocation = MapLocations.FirstOrDefault(); } } }
To perform geocoding, we need the GeocodeQuery class. So we embed that into a class with a method to perform the actual geocoding, a search string to holds the user input, a list of MapLocation (the output of GeocodeQuery) and SelectedLocation to the user’s selection.
Note there is also a SearchLocation property of type GeoCoordinate. That’s because the GeocodeQuery also needs a location to start searching from. If the programmer using my model doesn’t set it, I choose a default value. But you can imagine this being useful if someone just enters ‘Amersfoort’ for SearchText and a coordinate somewhere in the Netherlands – that way the GeocodeQuery knows that you want to have Amersfoort in the Netherlands, and not the Amersfoort in South Africa. Anyway, it’s now time for
Writing the search test
Add a new class GeocodeModelTest to NavigationDemo.Logic.Test and let’s write our first test:
using System; using System.Threading; using System.Windows; using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; using NavigationDemo.Logic.Models; namespace NavigationDemo.Logic.Test { [TestClass] public class GeocodeModelTest { [TestMethod] public void TestFindBoston() { var m = new GeocodeModel { SearchText = "Boston USA" }; var waitHandle = new AutoResetEvent(false); Deployment.Current.Dispatcher.BeginInvoke(async () => { await m.SearchLocations(); waitHandle.Set(); }); waitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.AreEqual(m.SelectedLocation.GeoCoordinate.Latitude, 42, 1); Assert.AreEqual(m.SelectedLocation.GeoCoordinate.Longitude, -71, 1); Assert.AreEqual(m.SelectedLocation.Information.Address.City, "Boston"); Assert.AreEqual(m.SelectedLocation.Information.Address.State, "Massachusetts"); Assert.AreEqual(m.SelectedLocation.Information.Address.Country, "United States of America"); } } }
The GeocodeQuery runs async and needs to run on the UI thread as well. If you have no idea what I am fooling around here with the Dispatcher and the AutoResetEvent, please read this article first. Anyway, this test works. Boston is indeed on the east coast of the United States and still in Massachusetts. Most reassuring. Now let’s see if SilverlightSerializer will indeed serialize this.
Writing the serialization test – take one
The first part is basically a repeat of the first test – writing unit test sometimes involves a lot of boring copy & paste work – but the last part is different:[TestMethod] public void TestStoreAndRetrieveBoston() { var m = new GeocodeModel { SearchText = "Boston USA" }; var waitHandle = new AutoResetEvent(false); Deployment.Current.Dispatcher.BeginInvoke(async () => { await m.SearchLocations(); waitHandle.Set(); }); waitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.IsNotNull(m.SelectedLocation); // Actual test var h = new IsolatedStorageHelper<GeocodeModel>(); if (h.ExistsInStorage()) { h.DeletedFromStorage(); } h.SaveToStorage(m); var retrievedModel = h.RetrieveFromStorage(); Assert.AreEqual(retrievedModel.SelectedLocation.Information.Address.City, "Boston"); } }
Adding this test to GeocodeModelTest will reveal a major bummer – a couple of the classes that are returned by GeocodeQuery – starting with MapLocation - have private constructors and cannot be serialized. Our model cannot be serialized. The usual approach to this kind of problem is to write a kind of wrapper class that can be serialized. But… using MVVMLight you are most of the time making wrapper classes anyway – that’s what a ViewModel is, after all, so let’s use that.
Writing the serialization test - take two
First, adorn the stuff that cannot be serialized in the GeocodeModel with the [DoNotSerialize] attribute, like this:
[DoNotSerialize] public MapLocation SelectedLocation { get; set; } [DoNotSerialize] public Listand the test is reduced to this:MapLocations { get; set; }
[TestMethod] public void TestStoreAndRetrieveBoston() { var m = new GeocodeModel { SearchText = "Boston USA" }; // Actual test var h = new IsolatedStorageHelper(); if (h.ExistsInStorage()) { h.DeletedFromStorage(); } h.SaveToStorage(m); var retrievedModel = h.RetrieveFromStorage(); Assert.AreEqual(retrievedModel.SearchText, "Boston USA"); }
Hurray, this works, but the model’s results are now no longer storing stuff. MapLocations is empty, so is SelectedLocation, if they are deserialized. Bascially we are now only testing if indeed the search test is retained after storage and retrieval. Well, it is.
Enter the viewmodels
So far I mainly showed what does not work. Now it’s time to show what does. First, we make a viewmodel around MapLocation:
using System.Device.Location; using GalaSoft.MvvmLight; using Microsoft.Phone.Maps.Services; namespace NavigationDemo.Logic.ViewModels { public class MapLocationViewModel : ViewModelBase { public MapLocationViewModel() { } public MapLocationViewModel(MapLocation model) { var a = model.Information.Address; Address = string.Format("{0} {1} {2} {3} {4}", a.Street, a.HouseNumber, a.PostalCode, a.City,a.Country).Trim(); Location = model.GeoCoordinate; } private string address; public string Address { get { return address; } set { if (address != value) { address = value; RaisePropertyChanged(() => Address); } } } private GeoCoordinate location; public GeoCoordinate Location { get { return location; } set { if (location != value) { location = value; RaisePropertyChanged(() => Location); } } } } }
That takes care of the MapLocation not being serializable. Once it is initialized, it does no longer need the model anymore. Which is a good thing, since it cannot be serialized ;-). Next is the GeocodeViewModel itself:
using System.Collections.ObjectModel; using System.Threading.Tasks; using System.Windows.Input; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Command; using NavigationDemo.Logic.Models; using Wp7nl.Utilities; using System.Linq; namespace NavigationDemo.Logic.ViewModels { public class GeocodeViewModel : ViewModelBase { public GeocodeViewModel() { MapLocations = new ObservableCollection<MapLocationViewModel>(); } public string Name { get; set; } public GeocodeViewModel( GeocodeModel model) : this() { Model = model; } public GeocodeModel Model{get;set;} public ObservableCollection<MapLocationViewModel> MapLocations { get; set; } [DoNotSerialize] public string SearchText { get { return Model.SearchText; } set { if (Model.SearchText != value) { Model.SearchText = value; RaisePropertyChanged(() => SearchText); } } } private MapLocationViewModel selectedLocation; public MapLocationViewModel SelectedLocation { get { return selectedLocation; } set { if (selectedLocation != value) { selectedLocation = value; RaisePropertyChanged(() => SelectedLocation); } } } public async Task SearchLocations() { MapLocations.Clear(); SelectedLocation = null; await Model.SearchLocations(); MapLocations.AddRange(Model.MapLocations.Select( p=> new MapLocationViewModel(p))); SelectedLocation = MapLocations.FirstOrDefault(); } [DoNotSerialize] public ICommand SearchLocationCommand { get { return new RelayCommand(async () => await SearchLocation()); } } } }
Notice that the only attribute that is serialized by the model, is now marked [DoNotSerialize]. This is really important – since the model may not be around yet when deserializing takes place, it would result in a null reference. If you pass things to the model, let the model serialize it. If you don’t let the viewmodel take care of it.
Writing the search test for the viewmodel
So since we are now no longer testing the model but the viewmodel, I added a new class “GeocodeViewModeTest” to, well, test the viewmodel.
using System; using System.Threading; using System.Windows; using Microsoft.VisualStudio.TestPlatform.UnitTestFramework; using NavigationDemo.Logic.Models; using NavigationDemo.Logic.ViewModels; using Wp7nl.Utilities;
namespace NavigationDemo.Logic.Test { [TestClass] public class GeocodeViewModelTest { [TestMethod] public void TestFindBostonWithViewModel() { var vm = new GeocodeViewModel( new GeocodeModel { SearchText = "Boston USA" }); var waitHandle = new AutoResetEvent(false); Deployment.Current.Dispatcher.BeginInvoke(async () => { await vm.SearchLocations(); waitHandle.Set(); }); waitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.AreEqual(vm.SelectedLocation.Address, "Boston United States of America"); Assert.AreEqual(vm.SelectedLocation.Location.Latitude, 42, 1); Assert.AreEqual(vm.SelectedLocation.Location.Longitude, -71, 1); } } }
Lo and behold, this test succeeds as well. Now the second test is actually a lot more interesting:
[TestMethod] public void TestStoreAndRetrieveBostonWithViewModel() { var vm = new GeocodeViewModel( new GeocodeModel { SearchText = "Boston USA" }); var waitHandle = new AutoResetEvent(false); Deployment.Current.Dispatcher.BeginInvoke(async () => { await vm.SearchLocations(); waitHandle.Set(); }); waitHandle.WaitOne(TimeSpan.FromSeconds(5)); Assert.IsNotNull(vm.SelectedLocation); var h = new IsolatedStorageHelper<GeocodeViewModel>(); if (h.ExistsInStorage()) { h.DeletedFromStorage(); } h.SaveToStorage(vm); var retrievedViewModel = h.RetrieveFromStorage(); Assert.AreEqual(retrievedViewModel.SelectedLocation.Address, "Boston United States of America"); Assert.AreEqual( retrievedViewModel.SelectedLocation.Location.Latitude, 42, 1); Assert.AreEqual( retrievedViewModel.SelectedLocation.Location.Longitude, -71, 1); }
And indeed, after retrieving the viewmodel from storage, the same asserts are fired and the test passes. Success: we can now find location and tombstone
Conclusion
I showed you some basic geocoding – how to find a location using a text input. I hope I have showed you also that unit tests are not only a way to assure some basic code quality and behavior, but are also a way to determine ahead if things are going to work the way you envisioned. Unit test make scaffolding and proof-of-concept approach of development a lot easier – you need a lot less starting up an app, clicking the right things and then finding breakpoint-by-breakpoint what goes wrong. Quite early in my development stage I ran into the fact that some things were not serializable. Imagine finding that out when the whole app was already mostly done, and then somewhere deep down something goes wrong with the tombstoning. Not fun.
Complete code – that is, complete for such an incomplete app – can be found here. Next time, we will do some actual navigation.
To prevent flames from Test Driven Design (TDD) purists: a variant of unit tests are integration test. Technically a unit test tests only tiny things that have no relation to another, like one object, method or property. Integration tests test the workings of larger pieces of code. So technically I am mostly writing integration tests. There, I’ve said it.