在执行UI测试期间在模拟器中更新位置

时间:2016-08-09 17:11:40

标签: ios swift cllocationmanager simulator xcode-ui-testing

我想在我的UI测试中更新模拟器的位置,以便我可以在位置更改时检查行为。有没有办法让UI测试以某种方式“调用”来运行可以通过模拟器的Debug / Location菜单项或其他方法更改模拟器位置的AppleScript?

如果我不能这样做,我正在考虑将自己的CLLocationManager版本注入应用程序,然后从UI测试(例如通过本地Web服务器)向该版本发送位置,假设有某种方式我可以从UI测试中“获取”位置信息(例如,通过写入Mac上的文件)。

3 个答案:

答案 0 :(得分:2)

您可以使用自定义GPX file模拟位置更改。使用您所需的路径或路线创建一个,然后从Product - >中选择它。方案 - >编辑方案......

enter image description here

答案 1 :(得分:2)

在Test中选择一个,选择位置时应该选择测试目标。在此之前,您应该拥有所需位置的gpx文件。网上有wbsites可以为您的位置生成一个。enter image description here

答案 2 :(得分:0)

我知道这应该更容易,但我找不到一个不那么复杂的解决方案。但它很灵活且有效 基本思想是使用帮助应用程序将测试位置发送到被测试的应用程序 以下是必需的步骤:

1)设置帮助应用程序:

这是一个在viewView中显示测试位置的单一视图应用程序:

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

如果在tableView中点击一行,帮助应用程序会尝试打开包含相应测试位置坐标的自定义URL。

2)准备要测试的应用以处理此自定义网址:

在info.plist中插入以下行:

enter image description here

注册自定义网址“setFakeLocation://”。为了使受测试的应用可以处理此网址,AppDelegate中的以下两个函数必须返回true

    func application(_ application: UIApplication, 
                                     willFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool {
        return true
    }

    func application(_ application: UIApplication, 
                                     didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        return true
    }  

此外,必须实现实际打开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
}  

基本上,它从URL中提取坐标,并在位置管理器的委托中调用函数didUpdateLocations,就好像真实位置管理器已更新位置一样。

3)设置CLLocationManager的子类:

但是,受测试的应用很可能会访问位置管理器的location属性,该属性也必须设置。在CLLocationManager中,此属性是只读的,无法设置。因此,必须使用CLLocationManager的自定义子类,并覆盖此属性:

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

在正常操作中,未设置子类的属性location,即nil。因此,当读取它的超类的属性location时,将读取真实的CLLocationManager。但是,如果在测试期间将此属性设置为测试位置,则将返回此测试位置。

4)在测试中的应用的UI测试中选择一个测试位置:

这可以通过多应用程序UI测试实现。它需要访问帮助应用程序:

在UITests中,在class ShopEasyUITests: XCTestCase中,定义属性

var helperApp: XCUIApplication!

并为其分配通过其bundleIdentifier

定义的应用程序
helperApp = XCUIApplication(bundleIdentifier: "com.yourCompany.Helperapp“)

此外,定义一个函数,通过激活帮助应用程序并点击tableView中的请求行来设置所需的测试位置,然后切换回测试中的应用程序:

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
}

最后,调用此函数:

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)进一步评论:

当然,正在测试的应用程序和帮助应用程序必须安装在同一个模拟器上。否则两个应用程序都无法通信。

可能相同的方法可用于UI测试推送通知。不是在locationManagerDelegate?.locationManager(locationManager, didUpdateLocations: [location])中调用func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool,而是可以在那里调用

func application(_ application: UIApplication, 
               didReceiveRemoteNotification userInfo: [AnyHashable: Any], 
                                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void)