Allow XCTest to simulate location

Alexey Alter-Pesotskiy
testableapple
Published in
4 min readDec 4, 2022

This Note originally published on my Personal Blog here. Read original note so that you won’t miss any content.

Many apps track users’ location. Some of them use it for navigation, some — for analytics. Is this for our good? Emm…

But in order to autotest related features, we need to mock some things. Today, I’ll show you how to do it in XCTest.

Precondition

Install any local web server (e.g.: Sinatra):

gem install sinatra

Configure the server

First of all, we need to create an endpoint that will receive a request with all the required parameters. In our case, it will be a POST request with the following params:

  • latitude — latitude of the initial location (default: 53.337)
  • longitude — longitude of the initial location (default: -6.27)
  • direction — direction to move (default: ”up”)
  • pace — movement speed (default: 0.0001)
  • duration — how long to move (default: 5 sec)

Then, depending on the direction parameter, we will move the location in the specified direction (within the given duration). For example, if the direction is ”up”, we will increase the latitude by the pace value. If the direction is ”left”, we will decrease the longitude by the pace value. And so on for the other directions.

To make it real, implement the following code into the server.rb file:

require 'json'
require 'sinatra'

post '/move' do
body = JSON.parse(request.body.read)
movement_direction = body['direction'] || 'up'
movement_duration = body['duration'] || 5
movement_pace = body['pace'] || 0.0001
initial_latitude = body['latitude'] || 53.337
initial_longtitude = body['longtitude'] || -6.27

move(
latitude: initial_latitude,
longtitude: initial_longtitude,
direction: movement_direction,
pace: movement_pace,
duration: movement_duration
)
end

def move(latitude:, longtitude:, pace:, direction:, duration:)
time_to_finish_movement = Time.now.to_i + duration
while Time.now.to_i < time_to_finish_movement
case direction
when 'up'
latitude += pace
when 'down'
latitude -= pace
when 'right'
longtitude += pace
when 'left'
longtitude -= pace
end

puts "Moving #{direction} to #{latitude},#{longtitude}"
`xcrun simctl location #{params['udid']} set #{latitude},#{longtitude}`
end
end

Link XCTest to the server

Alrighty, now we need to link our sandbox (XCTest) to the server. For this, we need to create a sendRequest function, that will get the required parameters and send a request to the server:

func sendRequest(endpoint: String, body: [String: Any], method: String = "POST") {
let udid = ProcessInfo.processInfo.environment["SIMULATOR_UDID"] ?? ""
let urlString = "\(host):\(port)\(endpoint)?udid=\(udid)"
guard let url = URL(string: urlString) else { return }

var request = URLRequest(url: url)
request.httpMethod = method
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
URLSession.shared.dataTask(with: request).resume()
}

The whole test class without tests will look like this:

import XCTest

class LocationTests: XCTestCase {
let app = XCUIApplication(bundleIdentifier: "com.apple.Maps")
let host = "http://localhost"
let port: UInt16 = 4567
let movementDuration: UInt32 = 5

override func setUpWithError() throws {
app.launch()
}

func move(_ direction: Direction, for duration: UInt32) {
sendRequest(
endpoint: "/move",
body: ["direction": direction.rawValue, "duration": duration]
)
}

enum Direction: String {
case right
case left
case up
case down
}

func sendRequest(endpoint: String, body: [String: Any], method: String = "POST") {
let urlString = "\(host):\(port)\(endpoint)"
guard let url = URL(string: urlString) else { return }

var request = URLRequest(url: url)
request.httpMethod = method
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
URLSession.shared.dataTask(with: request).resume()
}
}

You may notice that I used the default iOS Maps app as an example, feel free to replace it with the app of your choice.

Create the tests

So, it’s time to add some tests to make sure it all works together. Let’s create a simple one that will check if the user is able to move across the map in the right direction:

func testThatUserCanMoveRight() {
move(.right, for: movementDuration)
let myLocationFlag = app.otherElements["AnnotationContainer"].otherElements.firstMatch
let initialLocation = CGPoint(x: myLocationFlag.frame.midX, y: myLocationFlag.frame.midY)
sleep(movementDuration)
let newLocation = CGPoint(x: myLocationFlag.frame.midX, y: myLocationFlag.frame.midY)
XCTAssertGreaterThan(newLocation.x, initialLocation.x, "My location flag should move 'right'")
}

And one more test just to double-check that all good also with the longitude. To do that, let’s check the down direction:

func testThatUserCanMoveDown() {
move(.down, for: movementDuration)
let myLocationFlag = app.otherElements["AnnotationContainer"].otherElements.firstMatch
let initialLocation = CGPoint(x: myLocationFlag.frame.midX, y: myLocationFlag.frame.midY)
sleep(movementDuration)
let newLocation = CGPoint(x: myLocationFlag.frame.midX, y: myLocationFlag.frame.midY)
XCTAssertGreaterThan(newLocation.y, initialLocation.y, "My location flag should move 'down'")
}

Start the server

ruby server.rb

Run the tests

  1. Open Xcode
  2. Click on the `Run test` icon next to the testThatUserCanMoveRight function
  3. If everything was done correctly, you’ll probably see something like this:

4. Same for testThatUserCanMoveDown:

5. Profit!

Feel free to adjust the source code to your needs and don’t hesitate to reach out if you have any questions. See ya!

--

--