Search code examples
iosswiftgpsautomated-testsxctest

Programmatically simulate GPS location in iOS tests


I would like to write UI tests in Swift that make screenshots of various places of a map in our app. In order to do this, I need to simulate fake GPS data during the test.

There are some solutions like this one (https://blackpixel.com/writing/2016/05/simulating-locations-with-xcode-revisited.html) that use GPX files and simulate the location with Debug > Simulate Location in Xcode, but I need this to be completely automated. Ideal would be something similar to the LocationManager in Android.


Solution

  • I have had similar problems when writing UI tests, because the simulator / tethered device can't do everything you might want. What I do is write mocks that mimic the desired behavior (of something I would normally have no control over).

    Substituting a custom location manager for the CLLocationManager will allow you to take full control of location updates, as you can programmatically send the location updates through the CLLocationManagerDelegate method: locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]).

    Create a class MyLocationManager, make it a subclass of CLLocationManager, and have it override all the methods you call. Don't call super in the overridden methods, as CLLocationManager should never actually receive a method call.

    class MyLocationManager: CLLocationManager {
      override func requestWhenInUseAuthorization() {
        // Do nothing.
      }
    
      override func startUpdatingLocation() {
        // Begin location updates. You can use a timer to regularly send the didUpdateLocations method to the delegate, cycling through an array of type CLLocation.
      }
    
      // Override all other methods used.
    
    }
    

    The delegate property doesn't need to be overridden (and can't be), but you have access to it as a subclass of CLLocationManager.

    To use MyLocationManager you should pass in launch arguments that tell your app if it is a UITest or not. In your test case's setUp method insert this line of code:

    app.launchArguments.append("is_ui_testing")
    

    Store CLLocationManager as a property that is a MyLocationManager when testing. When not testing CLLocationManager will be used as normal.

    static var locationManger: CLLocationManager = ProcessInfo.processInfo.arguments.contains("is_ui_testing") ? MyLocationManager() : CLLocationManager()