Code-Driven Location Simulation
The Windows Phone emulator’s location
simulator is useful for testing your app while it is executing within
the emulator. To test the geo location features of your app on a
physical device or within a unit test requires a different code-driven
approach.
This section demonstrates how to abstract the use of the Geolocator class so that it can be replaced by a testable mock class.
On first glance you might think that mocking
the SDK’s geo location API should be a simple affair. Many of the
classes in the API are, however, sealed—including the Geolocator
class. This makes mocking them awkward because they must be wrapped or accessed via custom proxy classes.
The Geolocator
class implements an internal IGeolocator
interface, which would make an excellent candidate for abstracting the duties of the Geolocator
class. Unfortunately, IGeolocator
is marked internal, putting it out of our reach.
So the downloadable sample code includes a custom IGeoLocator
interface, which is almost identical to the SDK’s IGeolocator
interface (see Listing 2).
LISTING 2. IGeoLocator
Interface
public interface IGeoLocator
{
event EventHandler<PositionChangedProxyEventArgs> PositionChanged;
event EventHandler<StatusChangedProxyEventArgs> StatusChanged;
Task<GeopositionWrapper> GetGeoCoordinateAsync();
Task<GeopositionWrapper> GetGeoCoordinateAsync(
TimeSpan maximumAge, TimeSpan timeout);
PositionAccuracy DesiredAccuracy { get; set; }
PositionStatus LocationStatus { get; }
double MovementThreshold { get; set; }
uint ReportInterval { get; set; }
void Start();
void Stop();
}
A custom GeolocatorProxy
class wraps the built-in Geolocator
object. GeolocatorProxy
implements the IGeoLocator
interface (see Listing 3). GeolocatorProxy
is designed to be used in place of the Geolocator
class. It offers all the features of the built-in Geolocator
, but can be swapped out during testing.
Although the built-in Geolocator
class does not include methods for starting and stopping location tracking, implementations of the custom IGeoLocator
interface do. GeolocatorProxy
subscribes to its Geolocator
events in the Start
method and unsubscribes in the Stop
method.
The PositionChanged
and StatusChanged
events of the built-in Geolocator
class use a TypedEventHandler<T>
as the delegate type rather than the familiar EventHandler
. The appearance of the TypedEventHandler
is new to the Windows Phone 8 SDK and several APIs, including the geo
location API, make use of it. This proves troublesome when working with
Reactive Extensions (Rx). Thus, the GeolocatorProxy
exposes the events using the nongeneric EventHandler
class.
One last obstacle to abstracting the geo
location API is that some of the types in the SDK’s geo location API do
not provide the means for setting their members. These classes include,
in particular, the PositionChangedEventArgs
, StatusChangedEventArgs
, and the Geoposition
classes, which have been replaced with more flexible custom types to accommodate our mocking framework.
LISTING 3. GeolocatorProxy
Class (Excerpt)
public sealed class GeolocatorProxy : IGeoLocator
{
readonly Geolocator geolocator = new Geolocator();
...
public uint ReportInterval
{
get
{
return geolocator.ReportInterval;
}
set
{
geolocator.ReportInterval = value;
}
}
public void Start()
{
Stop();
geolocator.PositionChanged += HandlePositionChanged;
geolocator.StatusChanged += HandleStatusChanged;
}
public void Stop()
{
geolocator.PositionChanged -= HandlePositionChanged;
geolocator.StatusChanged -= HandleStatusChanged;
}
public event EventHandler<PositionChangedProxyEventArgs> PositionChanged;
void OnPositionChanged(PositionChangedProxyEventArgs e)
{
PositionChanged.Raise(this, e);
}
void HandlePositionChanged(Geolocator sender, PositionChangedEventArgs args)
{
OnPositionChanged(new PositionChangedProxyEventArgs(
new GeopositionWrapper(args.Position)));
}
...
}
The custom MockGeoLocator
mimics the behavior of the built-in Geolocator
and allows you to supply a list of GeoCoordinate
values that represent a predefined route and are analogous to the location simulator’s map points.
In addition to the properties of the IGeoLocator
interface, MockGeoLocator
has the following three properties that allow you to tailor when the PositionChanged
event is raised:
- InitializationMs—The time, in milliseconds, before transitioning the Status
to the NoData state
- FixDelayMs—The time before the transitioning the Status
from the NoData state to the Ready state
- PositionChangedDelayMs—The time interval, in milliseconds, between PositionChanged
events
MockGeoLocator
allows you to add waypoints to a list of GeoCoordinate
values using its WayPoints
property.
The MockGeoLocator.WalkPath
method runs on a background thread and processes the waypoints sequentially (see Listing 4).
If there are no waypoints specified, the MockGeoLocator
uses a default list of coordinates.
The first time WalkPath
is called, the class simulates initialization by pausing for the duration specified by the InitializationMs
value. It then updates its Status
to Ready and iterates over the WayPoints
collection, raising the PositionChanged
event for each one.
LISTING 4. MockGeoLocator.WalkPath
Method
void WalkPath()
{
if (wayPoints.Count < 1)
{
List<GeoCoordinate> defaultWayPoints = GetDefaultCoordinates();
wayPoints.AddRange(defaultWayPoints);
}
if (firstRun)
{
Status = PositionStatus.Initializing;
Wait(InitializationMs);
}
Status = PositionStatus.NoData;
Wait(FixDelayMs);
GeoCoordinate coordinate = wayPoints.First();
if (firstRun)
{
Position = new Position<GeoCoordinate>(
DateTimeOffset.Now, coordinate);
firstRun = false;
}
Status = PositionStatus.Ready;
Wait(PositionChangeDelayMs);
int index = 1;
while (started)
{
if (wayPoints != null && wayPoints.Count > index)
{
coordinate = wayPoints[index++];
}
SetPosition(coordinate);
Wait(PositionChangeDelayMs);
}
}
The Wait
method uses the Monitor
to block the background thread for the specified number of milliseconds (see Listing 5).
Tip
Using the Monitor
to block the thread is preferable to calling Thread.Sleep
because, unlike Monitor.Wait
, Thread.Sleep
cannot be unblocked.
LISTING 5. Wait
Method
static readonly object waitLock = new object();
static void Wait(int delayMs)
{
if (delayMs > 0)
{
lock (waitLock)
{
Monitor.Wait(waitLock, delayMs);
}
}
}
Both MockGeoLocator
and the GeolocatorProxy
implement IGeoLocator
. This allows you to use either type without referring to a concrete implementation, which is demonstrated in the next section.