Testing GPS location based mobile apps

Jeroen van Dijk
Jeroen van Dijk

6 juli 2017

It doesn’t matter if you call it mocking, spoofing or faking your GPS… testing location based services for mobile apps ain’t easy!

For one of our clients we develop a location based app that uses geofences to trigger contextual information with awesome results. While developing the visual part of that app in the iOS and Android simulator is easily done, building and testing the geofence logic is much harder.

For a long time we relied on the simulator options to set a location to trigger the geofence. And while that worked, it was labour intensive, slow, difficult to automate and incomparable to how mobile devices act in reality.

When we also started to use the motion sensors of the mobile device, testing the logic in a simulator became virtually impossible.

Location updates from a desktop to a mobile device

In our use case we want to bike a predefined route at a certain speed. The speed is an important aspect because our geofences are small, which can result in misses when moving at high speed. Next to that, errors of course always only occur in production, so you want to mimic the production data as close as possible.

So technically we continuously want to send location updates, at a predefined interval, from a desktop to a physical mobile device. But both the iOS and Android ecosystem have no direct solution in place for this.

Pokemon Go for the win…

Pokemon Go GPS

Since Pokemon Go was introduced in July 2016, searching for ‘fake gps‘, ‘mock gps‘ or ‘spoof gps‘ will mostly result in Pokemon Go related links that want to bypass the location based aspect of the game. Of course we’re not interested in hatching more eggs, but the hacks developers come up with are very cool.

Virtual location calculation

Based on our research we found solutions for both iOS and Android which we combine in a NodeJS script to execute commands for the ecosystem we want to test against. In essence it comes down to this:

import geolib from 'geolib';

const speed = 25;
let previous = {
    coord: null,
    delay: 0,
};

[array of coordinates].forEach((coordinate) => {
    previous = nextPoint(previous.coord, {
        latitude: coordinate[0],
        longitude: coordinate[1],
    }, previous.delay);
});

function nextPoint(current, next, delay) {
    let distance = 0,
        bearing = 0,
        intermediate,
        nextMove = delay;

    if (current !== null) {
        distance = geolib.getDistance(current, next);

        if (distance > speed) {
            nextMove = 1; // move in meters per second
            bearing = geolib.getBearing(current, next);
            intermediate = geolib.computeDestinationPoint(current, speed, bearing);
            locationUpdate(intermediate, nextMove);
            return nextPoint(intermediate, next, nextMove);
        }
        nextMove = (distance / speed);
    }

    locationUpdate(next, nextMove);

    return {
        coord: next,
        delay: nextMove,
    };
}

Main take away from this script is the virtual location calculation based on a given speed. The Node geolib library is a perfect solution for this. Using this library we’re able to calculate additional points to mimic a given speed. When the points are calculated the location has to be send to the device.

import fs from 'fs';
import process from 'child_process';
import sleep from 'system-sleep';

function locationUpdate(coord, delay) {
    sleep(delay * 1000);
  
    if (platform === 'ios') {
        fs.writeFileSync('cli-spoof-location.gpx',
            `<gpx creator="Xcode" version="1.1"><wpt lat="${coord.latitude.toFixed(6)}" lon="${coord.longitude.toFixed(6)}"><name>cli-spoof-location</name></wpt></gpx>`
        );
        process.exec('osascript autoclick.applescript', (error) => {
            if (error) {
                console.log(error.message);
                return;
            }
        });
        return;
    }

    if (platform === 'android') {
        // https://github.com/amotzte/android-mock-location-for-development
        process.exec(`adb shell am broadcast -a send.mock -e lat ${coord.latitude} -e lon ${coord.longitude}`,
            (error) => {
                if (error) {
                    console.log(error.message);
                    return;
                }
            }
        );
    }
}

What you see happening here is that the defined speed calculates a delay which we need to wait before sending a location update to the device. So we’re really blocking the NodeJS execution here.

Next to that we use an alternate app for Android to listen to location updates that we can send via ‘adb’. To be able to use this you’ll have to install and select this app as the app that is allowed to mock locations on the Android device. But the injected location will be used for every app on the device.

The solution for iOS is even more cumbersome, because Apple only allows location mocking via Xcode. You’ll have to add a GPX file to the project to fake a location or route. Because of this we continuously change the data in the GPX file and then script the click action in Xcode using:

tell application "System Events" to tell process "Xcode"
    click menu item "cli-spoof-location" of menu 1 of menu item "Simulate Location" of menu 1 of menu bar item "Debug" of menu bar 1
end tell

Using this strategy has dramatically lowered our field testing time, while increasing the quality of our updates to the app.

One more thing… as a bonus

Because we’re using Titanium as our solution to create near-native apps, the Xcode project gets recreated on every clean build. Of course we don’t want to manually insert the GPX file reference over and over again, so we created a Titanium CLI plugin.

exports.id = 'cli-spoof.test';
exports.cliVersion = '>=3.2';
exports.init = function (logger, config, cli, appc) {
    cli.on('build.ios.xcodeproject', {
        pre: function (data) {
            // don't alter the project when going to production
            if (data.ctx.deployType === 'production') {
                return;
            }

            logger.info('Add GPX file to project for testing purposes');

            var uniqueHex = this.hash('cli-spoof-location.gpx').substr(0, 24).toUpperCase();

            var hash = data.args[0].hash;
            var objects = hash.project.objects;

            var PBXFileReference = objects['PBXFileReference'];
            var PBXGroup = objects['PBXGroup'];
            var mainGroup = objects['PBXProject'][hash.project.rootObject].mainGroup;

            PBXFileReference[uniqueHex] = {
                isa: 'PBXFileReference',
                fileEncoding: 4,
                lastKnownFileType: 'text',
                name: 'cli-spoof-location.gpx',
                path: 'path/to/gpx/cli-spoof-location.gpx',
                sourceTree: '"<group>"'
            };
            PBXFileReference[uniqueHex + '_comment'] = 'cli-spoof-location.gpx';
            PBXGroup[mainGroup].children.unshift({
                value: uniqueHex,
                comment: 'cli-spoof-location.gpx'
            });
        }
    });
};

Add this to your tiapp.xml plugin section to be able to alter the generated Xcode project adding the reference to the GPX file on the fly.

Reference links

All credits for our solution go to the people and companies behind the contents of these links: