Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating location in simulator during executing of UI Test

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).

like image 224
Peter Alfvin Avatar asked Aug 09 '16 17:08

Peter Alfvin


People also ask

Does location work on simulator?

Location simulation is available for iOS and tvOS devices and simulators.

How do you simulate a location in Simulation?

Click the location on the map you want to simulate, and then press the lowercase L key. A dialog will appear with the message, “Simulated Location Set”. After a short time, the simulated location you set in the Editor will appear as a pink dot in the Android Emulator. Click anywhere on the map, and press Shift+L.

How do you run a UI test?

When you're ready to test, go to a test class and place the cursor inside the test method to record the interaction. From the debug bar, click the Record UI Test button. Xcode will launch the app and run it. You can interact with the element on-screen and perform a sequence of interactions for any UI test.

How do I add a UI test to Xcode project?

The easiest way to add a unit test target to your project is to select the Include Tests checkbox when you create the project. Selecting the checkbox creates targets for unit tests and UI tests. To add a unit test target to an existing Xcode project, choose File > New > Target.


3 Answers

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

like image 147
Prabhu.Somasundaram Avatar answered Oct 26 '22 23:10

Prabhu.Somasundaram


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

like image 36
Joe Masilotti Avatar answered Oct 26 '22 23:10

Joe Masilotti


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) 
like image 45
Reinhard Männer Avatar answered Oct 27 '22 01:10

Reinhard Männer