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).
Location simulation is available for iOS and tvOS devices and simulators.
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.
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.
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.
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.
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...
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:
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With