3 minute read

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

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

  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:

    Moving right

  4. Same for testThatUserCanMoveDown:

    Moving down

  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!

Updated: