Allow XCTest to simulate location
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 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()
}
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 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()
}
}
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
- Open Xcode
- Click on the
Run test
icon next to thetestThatUserCanMoveRight
function -
If everything was done correctly, you’ll probably see something like this:
-
Same for
testThatUserCanMoveDown
: - 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!