在 iOS 测试中以编程方式模拟 GPS 位置
Programmatically simulate GPS location in iOS tests
我想在 Swift 中编写 UI 测试,在我们的应用程序中制作地图不同位置的屏幕截图。为此,我需要在测试期间模拟假的 GPS 数据。
有一些像这个 (https://blackpixel.com/writing/2016/05/simulating-locations-with-xcode-revisited.html) 的解决方案,它们使用 GPX 文件并在 Xcode 中使用 Debug > Simulate Location
模拟位置,但我需要它是完全自动化的。理想的是类似于 Android 中的 LocationManager
。
你不能。 CLLocationManager 在委托的帮助下为您提供位置,您可以通过任何方式设置此位置。
您可以制作一个 CLLocationManager 模拟器 class,它随时间提供一些位置。
或者您可以使用带时间戳的 GPX 同步您的测试。
我在编写 UI 测试时遇到过类似的问题,因为模拟器/系留设备无法完成您可能想要的一切。我所做的是编写模拟所需行为的模拟(我通常无法控制的行为)。
用自定义位置管理器替换 CLLocationManager 将使您能够完全控制位置更新,因为您可以通过 CLLocationManagerDelegate 方法以编程方式发送位置更新:locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
.
创建一个 class MyLocationManager
,使其成为 CLLocationManager
的子 class,并让它覆盖您调用的所有方法。不要在覆盖的方法中调用 super,因为 CLLocationManager 不应该实际接收方法调用。
class MyLocationManager: CLLocationManager {
override func requestWhenInUseAuthorization() {
// Do nothing.
}
override func startUpdatingLocation() {
// Begin location updates. You can use a timer to regularly send the didUpdateLocations method to the delegate, cycling through an array of type CLLocation.
}
// Override all other methods used.
}
delegate
属性 不需要被覆盖(也不可能),但您可以作为 CLLocationManager 的子class 访问它。
要使用 MyLocationManager
,您应该传入启动参数,告诉您的应用它是否是 UITest。在您的测试用例的 setUp
方法中插入这行代码:
app.launchArguments.append("is_ui_testing")
将 CLLocationManager 存储为 属性,即测试时的 MyLocationManager。不测试时,CLLocationManager 将正常使用。
static var locationManger: CLLocationManager = ProcessInfo.processInfo.arguments.contains("is_ui_testing") ? MyLocationManager() : CLLocationManager()
仅出于(单元)测试的目的,您可以调配 startUpdatingLocation
方法以指示 CLLocationManager
实例按您希望的方式运行。
这是一个可能的实现:
// Just a wrapper to allow storing weak references in an array
struct WeakRef<T: AnyObject> {
weak var value: T?
}
extension CLLocationManager {
// Replace the `startUpdatingLocation` method with our own
private static var swizzled = false
static func swizzle() {
guard !swizzled else { return }
swizzled = true
method_exchangeImplementations(class_getInstanceMethod(self, #selector(startUpdatingLocation))!,
class_getInstanceMethod(self, #selector(swizzledStartUpdatingLocation))!)
}
// When a CLLocationManager is asked to `startUpdatingLocation`,
// it adds itself to the list of registered instances
// Using weak references to allow managers to deallocate when needed
static var registeredInstances = [WeakRef<CLLocationManager>]()
@objc func swizzledStartUpdatingLocation() {
Self.registeredInstances.append(WeakRef(value: self))
if let mockedLocation = Self.mockedLocation {
delegate?.locationManager?(self, didUpdateLocations: [mockedLocation])
}
}
// This is the mocked location, when it changes
// all registered managers notify their delegates with the new mocked location
static var mockedLocation: CLLocation? {
didSet {
guard let mockedLocation = mockedLocation else { return }
registeredInstances.forEach {
guard let manager = [=10=].value else { return }
manager.delegate?.locationManager?(manager, didUpdateLocations: [mockedLocation])
}
}
}
}
当然,这只是一个基本的实现 stopUpdatingLocation
也应该进行调整以删除管理器。 CLLocationMangager
的其他功能也可以在扩展中implemented/swizzled
我想在 Swift 中编写 UI 测试,在我们的应用程序中制作地图不同位置的屏幕截图。为此,我需要在测试期间模拟假的 GPS 数据。
有一些像这个 (https://blackpixel.com/writing/2016/05/simulating-locations-with-xcode-revisited.html) 的解决方案,它们使用 GPX 文件并在 Xcode 中使用 Debug > Simulate Location
模拟位置,但我需要它是完全自动化的。理想的是类似于 Android 中的 LocationManager
。
你不能。 CLLocationManager 在委托的帮助下为您提供位置,您可以通过任何方式设置此位置。
您可以制作一个 CLLocationManager 模拟器 class,它随时间提供一些位置。 或者您可以使用带时间戳的 GPX 同步您的测试。
我在编写 UI 测试时遇到过类似的问题,因为模拟器/系留设备无法完成您可能想要的一切。我所做的是编写模拟所需行为的模拟(我通常无法控制的行为)。
用自定义位置管理器替换 CLLocationManager 将使您能够完全控制位置更新,因为您可以通过 CLLocationManagerDelegate 方法以编程方式发送位置更新:locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])
.
创建一个 class MyLocationManager
,使其成为 CLLocationManager
的子 class,并让它覆盖您调用的所有方法。不要在覆盖的方法中调用 super,因为 CLLocationManager 不应该实际接收方法调用。
class MyLocationManager: CLLocationManager {
override func requestWhenInUseAuthorization() {
// Do nothing.
}
override func startUpdatingLocation() {
// Begin location updates. You can use a timer to regularly send the didUpdateLocations method to the delegate, cycling through an array of type CLLocation.
}
// Override all other methods used.
}
delegate
属性 不需要被覆盖(也不可能),但您可以作为 CLLocationManager 的子class 访问它。
要使用 MyLocationManager
,您应该传入启动参数,告诉您的应用它是否是 UITest。在您的测试用例的 setUp
方法中插入这行代码:
app.launchArguments.append("is_ui_testing")
将 CLLocationManager 存储为 属性,即测试时的 MyLocationManager。不测试时,CLLocationManager 将正常使用。
static var locationManger: CLLocationManager = ProcessInfo.processInfo.arguments.contains("is_ui_testing") ? MyLocationManager() : CLLocationManager()
仅出于(单元)测试的目的,您可以调配 startUpdatingLocation
方法以指示 CLLocationManager
实例按您希望的方式运行。
这是一个可能的实现:
// Just a wrapper to allow storing weak references in an array
struct WeakRef<T: AnyObject> {
weak var value: T?
}
extension CLLocationManager {
// Replace the `startUpdatingLocation` method with our own
private static var swizzled = false
static func swizzle() {
guard !swizzled else { return }
swizzled = true
method_exchangeImplementations(class_getInstanceMethod(self, #selector(startUpdatingLocation))!,
class_getInstanceMethod(self, #selector(swizzledStartUpdatingLocation))!)
}
// When a CLLocationManager is asked to `startUpdatingLocation`,
// it adds itself to the list of registered instances
// Using weak references to allow managers to deallocate when needed
static var registeredInstances = [WeakRef<CLLocationManager>]()
@objc func swizzledStartUpdatingLocation() {
Self.registeredInstances.append(WeakRef(value: self))
if let mockedLocation = Self.mockedLocation {
delegate?.locationManager?(self, didUpdateLocations: [mockedLocation])
}
}
// This is the mocked location, when it changes
// all registered managers notify their delegates with the new mocked location
static var mockedLocation: CLLocation? {
didSet {
guard let mockedLocation = mockedLocation else { return }
registeredInstances.forEach {
guard let manager = [=10=].value else { return }
manager.delegate?.locationManager?(manager, didUpdateLocations: [mockedLocation])
}
}
}
}
当然,这只是一个基本的实现 stopUpdatingLocation
也应该进行调整以删除管理器。 CLLocationMangager
的其他功能也可以在扩展中implemented/swizzled