Updating location in simulator during executing of UI Test
Asked Answered
I

4

8

I'd like to update the location of the simulator in the middle of my UI test so that I can check the behavior as the location changes. Is there a way for the UI Test to somehow "call out" to run an AppleScript that can change the simulator location through the simulator's Debug/Location menu item or some other method?

If I can't do that, I was thinking of injecting my own version of CLLocationManager into the app and then sending locations to that version from the UI test (e.g. via a local web server), assuming there is some way that I can get the location information "out" of the UI test (e.g. by writing to a file on the Mac).

Ironsmith answered 9/8, 2016 at 17:11 Comment(0)
J
5

As of iOS 16.4, macOS 13.3, and Xcode 14.3, a new XCTest API is available which lets you do this very easily.

https://developer.apple.com/documentation/xctest/xcuidevice/4111083-location

You can simulate the location of your device by setting the location property on an XCUIDevice.

Junta answered 16/5, 2023 at 17:1 Comment(1)
This answer deserves more upvotes :) Here's a snippet how I did it: XCUIDevice.shared.location = XCUILocation(location: CLLocation(latitude: 52.5200, longitude: 13.4050)) To make sure the change is actually visually there I needed to also add sleep after: sleep(1)Iliac
B
3

Pick the one in Test, It should be your test target selected when you pick the location. Before that you should have gpx file for which ever location you want. there are wbsites available online to generate the one for your location.enter image description here

Brophy answered 1/3, 2017 at 14:52 Comment(4)
See my comment on the previous answer. Am I missing something in what you are proposing?Ironsmith
You would duplicate the schemes for your need. To my knowledge we can switch locations through UI Test scripts.Brophy
This worked for me after I did a clean build and deleted the old app.Ineligible
This method only works for unit tests. UI tests require a new API released in iOS 16.4, macOS 13.3, and Xcode 14.3 developer.apple.com/documentation/xctest/xcuidevice/…Junta
F
2

You can simulate changes in location with a custom GPX file. Create one with your required path or route and select it from Product -> Scheme -> Edit Scheme...

enter image description here

Flood answered 9/8, 2016 at 20:47 Comment(3)
Thanks for the response. As I understand it, though, the most the GPX file can specify is what location to use at what time. I would like to specify the location in my tests, where each test may need to specify one or more locations during the execution of the test.Ironsmith
If I am right, in this way you can set up different schemes that use different locations, but it is not possible to change the location during a test run.Unrelenting
This method only works for unit tests. UI tests require a new API released in iOS 16.4, macOS 13.3, and Xcode 14.3 developer.apple.com/documentation/xctest/xcuidevice/…Junta
S
1

I know this should be much easier, but I could not find a solution that is less complicated. However it is flexible and works.
The basic idea is to use a helper app that sends a test location to the app under test.
Here are the required steps:

1) Set up a helper app:

This is a single view app that displays test locations in a tableView:

import UIKit
import CoreLocation

class ViewController: UIViewController {
    struct FakeLocation {
        let name: String
        let latitude: Double
        let longitude: Double
    }

    @IBOutlet weak var tableView: UITableView!
    var fakeLocations: [FakeLocation] = [
        FakeLocation.init(name: "Location 1", latitude: 8.0, longitude: 49.0),
        FakeLocation.init(name: "Location 2", latitude: 8.1, longitude: 49.1)
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
    }

} // class ViewController

// MARK: - Table view data source

extension ViewController: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fakeLocations.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell")!
        let row = indexPath.row
        let fakeLocation = fakeLocations[row]
        cell.textLabel?.text = fakeLocation.name
        return cell
    }

} // extension ViewController: UITableViewDataSource

extension ViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let cell = tableView.cellForRow(at: indexPath)
        cell?.isSelected = false
        let row = indexPath.row
        let fakeLocation = fakeLocations[row]
        let fakeLocationString = String.init(format: "%f, %f", fakeLocation.latitude, fakeLocation.longitude)
        let urlEncodedFakeLocationString = fakeLocationString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
        let url = URL.init(string: "setFakeLocation://" + urlEncodedFakeLocationString!)
        UIApplication.shared.open(url!) { (urlOpened) in
            guard urlOpened else { 
                print("could not send fake location") 
                return 
            }
        }
    }

} // extension ViewController: UITableViewDelegate

If one taps a row in the tableView, the helper app tries to open a custom URL that contains the coordinates of the corresponding test location.

2) Prepare the app under test to process this custom URL:

Inset in the info.plist the following rows:

enter image description here

This registers the custom URL "setFakeLocation://". In order that the app under test can process this URL, the following two functions in the AppDelegate have to return true:

    func application(_ application: UIApplication, 
                                     willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
        return true
    }

    func application(_ application: UIApplication, 
                                     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }  

Additionally, one has to implement the function that actually opens the URL:

func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {      
    let urlString = url.absoluteString
    let splittedString = urlString.components(separatedBy: "://")
    let coordinatesURLEncodedString = splittedString[1]
    let coordinateString = coordinatesURLEncodedString.removingPercentEncoding!
    let coordinateStrings = coordinateString.components(separatedBy: ", ")
    let latitude  = Double(coordinateStrings[0])!
    let longitude = Double(coordinateStrings[1])!
    let coordinate = CLLocationCoordinate2D.init(latitude: latitude, longitude: longitude)
    let location = CLLocation.init(coordinate: coordinate, 
                                   altitude: 0, 
                                   horizontalAccuracy: 0, 
                                   verticalAccuracy: 0, 
                                   timestamp: Date())
    let locationManager = LocationManager.shared
    locationManager.location = location
    locationManagerDelegate?.locationManager(locationManager, didUpdateLocations: [location])
    return true
}  

Essentially, it extracts the coordinates from the URL and calls in the delegate of the location manager the function didUpdateLocations as if the real location manager had updated the location.

3) Setup a subclass of CLLocationManager:

However, the app under test will most likely access the location property of the location manager, which had to be set also. In a CLLocationManager, this property is read only and cannot be set. Thus, one has to use a custom subclass of CLLocationManager, and override this property:

final class LocationManager: CLLocationManager {

  …

    // Allow location to be set for testing. If it is set, the set value will be returned, else the current location.
    private var _location: CLLocation?
    @objc dynamic override var location: CLLocation? {
        get { 
            let usedLocation = _location ?? super.location
            return usedLocation 
        }
        set {
            self._location = newValue
        }   
    }

  …

} // class LocationManager

In normal operation, the property location of the subclass is not set, i.e. nil. So when it is read, the property location of its superclass, the real CLLocationManager is read. If however during a test this property is set to a test location, this test location will be returned.

4) Select a test location in a UI test of the app under test:

This is possible with multi-app UI testing. It requires access to the helper app:

In the UITests, in class ShopEasyUITests: XCTestCase, define a property

var helperApp: XCUIApplication!

and assign to it the application that is defined via its bundleIdentifier

helperApp = XCUIApplication(bundleIdentifier: "com.yourCompany.Helperapp“)

Further, define a function that sets the required test location by activating the helper app and tapping the requested row in the tableView, and switches back to the app under test:

private func setFakeLocation(name: String) -> Bool {
    helperApp.activate() // Activate helper app
    let cell = helperApp.tables.cells.staticTexts[name]
    let cellExists = cell.waitForExistence(timeout: 10.0)
    if cellExists {
        cell.tap() // Tap the tableViewCell that displays the selected item
    }
    appUnderTest.activate() // Activate the app under test again
    return cellExists
}

Eventually, call this function:

func test_setFakeLocation() {
    // Test that the helper app can send a fake location, and this location is handled correctly.

    // when
    let fakeLocationWasTappedInHelperApp = setFakeLocation(name: "Location 1")

    // then
    XCTAssert(fakeLocationWasTappedInHelperApp)

    …
}

5) Further remarks:

Of course, the app under test and the helper app must be installed on the same simulator. Otherwise both apps could not communicate.

Probably the same approach can be used to UI test push notifications. Instead of calling locationManagerDelegate?.locationManager(locationManager, didUpdateLocations: [location]) in func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool, one could probably call there

func application(_ application: UIApplication, 
               didReceiveRemoteNotification userInfo: [AnyHashable: Any], 
                                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) 
Sauternes answered 15/4, 2018 at 11:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.