XCode UI 测试 swizzle API class 方法
XCode UI Testing swizzle API class methods
我有一个带有两个按钮的简单应用程序,可以调用 JSON 网络服务并打印出结果消息。
我想尝试新的 XCode 7 UI 测试,但我不明白如何模拟 API 请求。
为简单起见,我构建了一个没有实际请求或任何异步操作的示例。
我在主目标中有 ZZSomeAPI.swift
文件:
import Foundation
public class ZZSomeAPI: NSObject {
public class func call(parameter:String) -> Bool {
return true
}
}
然后我的ZZSomeClientViewController.swift
:
import UIKit
class ZZSomeClientViewController: UIViewController {
@IBAction func buttonClick(sender: AnyObject) {
print(ZZSomeAPI.call("A"))
}
}
现在我添加了一个 UI 测试目标,记录了点击按钮的过程,我有类似的东西:
import XCTest
class ZZSomeClientUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
func testCall() {
let app = XCUIApplication()
app.childrenMatchingType(.Window).elementBoundByIndex(0).childrenMatchingType(.Other).element.childrenMatchingType(.Other).elementBoundByIndex(1).childrenMatchingType(.Button).elementBoundByIndex(0).tap()
}
}
所以这有效,运行 测试将打印出 true
。但是我想在 API returns false
时包含一个测试而不弄乱 API。因此,我将 ZZSomeAPI.swift
添加到 UI 测试目标并尝试方法调配(UI测试代码已更新):
import XCTest
class ZZSomeClientUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
func testSwizzle() {
XCTAssert(ZZSomeAPI.call("a"))
XCTAssertFalse(ZZSomeAPI.callMock("a"))
XCTAssert(ZZSomeAPI.swizzleClass("call", withSelector: "callMock", forClass: ZZSomeAPI.self))
XCTAssertFalse(ZZSomeAPI.call("a"), "failed swizzle")
}
func testCall() {
XCTAssert(ZZSomeAPI.swizzleClass("call", withSelector: "callMock", forClass: ZZSomeAPI.self))
let app = XCUIApplication()
app.childrenMatchingType(.Window).elementBoundByIndex(0).childrenMatchingType(.Other).element.childrenMatchingType(.Other).elementBoundByIndex(1).childrenMatchingType(.Button).elementBoundByIndex(0).tap()
}
}
extension NSObject {
public class func swizzleClass(origSelector: String!, withSelector: String!, forClass:AnyClass!) -> Bool {
var originalMethod: Method?
var swizzledMethod: Method?
originalMethod = class_getClassMethod(forClass, Selector(origSelector))
swizzledMethod = class_getClassMethod(forClass, Selector(withSelector))
if (originalMethod == COpaquePointer(bitPattern: 0)) { return false }
if (swizzledMethod == COpaquePointer(bitPattern: 0)) { return false }
method_exchangeImplementations(originalMethod!, swizzledMethod!)
return true
}
}
extension ZZSomeAPI {
public class func callMock(parameter:String) -> Bool {
return false
}
}
所以,testSwizzle()
通过了,这意味着 swizzling 成功了。但是 testCall()
仍然打印 true
而不是 false
.
是因为当 UI测试和主要目标是两个不同的应用程序时,仅在测试目标上进行调配吗?
有什么解决办法吗?
我找到了 Mock API Requests Xcode 7 Swift Automated UI Testing 但我不确定如何在这里使用 launchArguments
。
在这个例子中只有一种情况,但我需要模拟 call()
方法以获得不同测试方法的不同结果......如果我使用 launchArgument
例如包含完整响应的 MOCK_API_RESPONSE
要返回,主要目标应用程序委托将有一些 "ugly test-only" 代码...有没有办法检查(在主要目标中)它是否正在为 UITest 目标编译,所以它只包含那个模拟 launchArguments 的代码?
最干净的选择确实是让 swizzling 工作...
Xcode UI 测试在与您的应用程序不同的应用程序中执行。因此,在测试 运行ner 应用程序中对 类 的更改不会影响测试应用程序中的 类。
这与单元测试不同,在单元测试中,您的测试 运行 在您的应用程序进程中。
我已经接受了 Mats 的回答,因为实际问题(在 UI 测试中进行了调整)得到了回答。
但我最终得到的当前解决方案是使用在线模拟服务器 http://mocky.io/,因为混合目标是模拟远程 API 调用。
更新 ZZSomeAPI.swift
public
属性 API URL 而不是在方法中获取它:
import Foundation
public class ZZSomeAPI {
public static var apiURL: String = NSBundle.mainBundle().infoDictionary?["API_URL"] as! String
public class func call(parameter:String) -> Bool {
... use apiURL ...
}
}
然后更新了应用委托以使用 launchArguments
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSProcessInfo().arguments.contains("MOCK_API") { // for UI Testing
if let param = NSProcessInfo().environment["MOCK_API_URL"] {
ZZSomeAPI.apiURL = param
}
}
return true
}
}
然后在我的 UI 测试用例 class 中创建 setupAPIMockWith
以按需在 mocky.io 中创建模拟响应:
import XCTest
class ZZSomeClientUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
}
func setupAPIMockWith(jsonBody: NSDictionary) -> String {
let expectation = self.expectationWithDescription("mock request setup")
let request = NSMutableURLRequest(URL: NSURL(string: "http://www.mocky.io/")!)
request.HTTPMethod = "POST"
var theJSONText: NSString?
do {
let theJSONData = try NSJSONSerialization.dataWithJSONObject(jsonBody, options: NSJSONWritingOptions.PrettyPrinted)
theJSONText = NSString(data: theJSONData, encoding: NSUTF8StringEncoding)
} catch {
XCTFail("failed to serialize json body for mock setup")
}
let params = [
"statuscode": "200",
"location": "",
"contenttype": "application/json",
"charset": "UTF-8",
"body": theJSONText!
]
let body = params.map({
let key = [=12=].0.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
let value = [=12=].1.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
return "\(key!)=\(value!)"
}).joinWithSeparator("&")
request.HTTPBody = body.dataUsingEncoding(NSUTF8StringEncoding)
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
var url: String?
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
XCTAssertNil(error)
do {
let json: NSDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as! NSDictionary
XCTAssertNotNil(json["url"])
url = json["url"] as? String
} catch {
XCTFail("failed to parse mock setup json")
}
expectation.fulfill()
}
task.resume()
self.waitForExpectationsWithTimeout(5, handler: nil)
XCTAssertNotEqual(url, "")
return url!
}
func testCall() {
let app = XCUIApplication()
app.launchArguments.append("MOCK_API")
app.launchEnvironment = [
"MOCK_API_URL": self.setupAPIMockWith([
"msg": [
"code": -99,
"text": "yoyo"
]
])
]
app.launch()
app.childrenMatchingType(.Window).elementBoundByIndex(0).childrenMatchingType(.Other).element.childrenMatchingType(.Other).elementBoundByIndex(1).childrenMatchingType(.Button).elementBoundByIndex(0).tap()
app.staticTexts["yoyo"].tap()
}
}
我有一个带有两个按钮的简单应用程序,可以调用 JSON 网络服务并打印出结果消息。
我想尝试新的 XCode 7 UI 测试,但我不明白如何模拟 API 请求。
为简单起见,我构建了一个没有实际请求或任何异步操作的示例。
我在主目标中有 ZZSomeAPI.swift
文件:
import Foundation
public class ZZSomeAPI: NSObject {
public class func call(parameter:String) -> Bool {
return true
}
}
然后我的ZZSomeClientViewController.swift
:
import UIKit
class ZZSomeClientViewController: UIViewController {
@IBAction func buttonClick(sender: AnyObject) {
print(ZZSomeAPI.call("A"))
}
}
现在我添加了一个 UI 测试目标,记录了点击按钮的过程,我有类似的东西:
import XCTest
class ZZSomeClientUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
func testCall() {
let app = XCUIApplication()
app.childrenMatchingType(.Window).elementBoundByIndex(0).childrenMatchingType(.Other).element.childrenMatchingType(.Other).elementBoundByIndex(1).childrenMatchingType(.Button).elementBoundByIndex(0).tap()
}
}
所以这有效,运行 测试将打印出 true
。但是我想在 API returns false
时包含一个测试而不弄乱 API。因此,我将 ZZSomeAPI.swift
添加到 UI 测试目标并尝试方法调配(UI测试代码已更新):
import XCTest
class ZZSomeClientUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
}
func testSwizzle() {
XCTAssert(ZZSomeAPI.call("a"))
XCTAssertFalse(ZZSomeAPI.callMock("a"))
XCTAssert(ZZSomeAPI.swizzleClass("call", withSelector: "callMock", forClass: ZZSomeAPI.self))
XCTAssertFalse(ZZSomeAPI.call("a"), "failed swizzle")
}
func testCall() {
XCTAssert(ZZSomeAPI.swizzleClass("call", withSelector: "callMock", forClass: ZZSomeAPI.self))
let app = XCUIApplication()
app.childrenMatchingType(.Window).elementBoundByIndex(0).childrenMatchingType(.Other).element.childrenMatchingType(.Other).elementBoundByIndex(1).childrenMatchingType(.Button).elementBoundByIndex(0).tap()
}
}
extension NSObject {
public class func swizzleClass(origSelector: String!, withSelector: String!, forClass:AnyClass!) -> Bool {
var originalMethod: Method?
var swizzledMethod: Method?
originalMethod = class_getClassMethod(forClass, Selector(origSelector))
swizzledMethod = class_getClassMethod(forClass, Selector(withSelector))
if (originalMethod == COpaquePointer(bitPattern: 0)) { return false }
if (swizzledMethod == COpaquePointer(bitPattern: 0)) { return false }
method_exchangeImplementations(originalMethod!, swizzledMethod!)
return true
}
}
extension ZZSomeAPI {
public class func callMock(parameter:String) -> Bool {
return false
}
}
所以,testSwizzle()
通过了,这意味着 swizzling 成功了。但是 testCall()
仍然打印 true
而不是 false
.
是因为当 UI测试和主要目标是两个不同的应用程序时,仅在测试目标上进行调配吗?
有什么解决办法吗?
我找到了 Mock API Requests Xcode 7 Swift Automated UI Testing 但我不确定如何在这里使用 launchArguments
。
在这个例子中只有一种情况,但我需要模拟 call()
方法以获得不同测试方法的不同结果......如果我使用 launchArgument
例如包含完整响应的 MOCK_API_RESPONSE
要返回,主要目标应用程序委托将有一些 "ugly test-only" 代码...有没有办法检查(在主要目标中)它是否正在为 UITest 目标编译,所以它只包含那个模拟 launchArguments 的代码?
最干净的选择确实是让 swizzling 工作...
Xcode UI 测试在与您的应用程序不同的应用程序中执行。因此,在测试 运行ner 应用程序中对 类 的更改不会影响测试应用程序中的 类。
这与单元测试不同,在单元测试中,您的测试 运行 在您的应用程序进程中。
我已经接受了 Mats 的回答,因为实际问题(在 UI 测试中进行了调整)得到了回答。
但我最终得到的当前解决方案是使用在线模拟服务器 http://mocky.io/,因为混合目标是模拟远程 API 调用。
更新 ZZSomeAPI.swift
public
属性 API URL 而不是在方法中获取它:
import Foundation
public class ZZSomeAPI {
public static var apiURL: String = NSBundle.mainBundle().infoDictionary?["API_URL"] as! String
public class func call(parameter:String) -> Bool {
... use apiURL ...
}
}
然后更新了应用委托以使用 launchArguments
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSProcessInfo().arguments.contains("MOCK_API") { // for UI Testing
if let param = NSProcessInfo().environment["MOCK_API_URL"] {
ZZSomeAPI.apiURL = param
}
}
return true
}
}
然后在我的 UI 测试用例 class 中创建 setupAPIMockWith
以按需在 mocky.io 中创建模拟响应:
import XCTest
class ZZSomeClientUITests: XCTestCase {
override func setUp() {
super.setUp()
continueAfterFailure = false
}
func setupAPIMockWith(jsonBody: NSDictionary) -> String {
let expectation = self.expectationWithDescription("mock request setup")
let request = NSMutableURLRequest(URL: NSURL(string: "http://www.mocky.io/")!)
request.HTTPMethod = "POST"
var theJSONText: NSString?
do {
let theJSONData = try NSJSONSerialization.dataWithJSONObject(jsonBody, options: NSJSONWritingOptions.PrettyPrinted)
theJSONText = NSString(data: theJSONData, encoding: NSUTF8StringEncoding)
} catch {
XCTFail("failed to serialize json body for mock setup")
}
let params = [
"statuscode": "200",
"location": "",
"contenttype": "application/json",
"charset": "UTF-8",
"body": theJSONText!
]
let body = params.map({
let key = [=12=].0.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
let value = [=12=].1.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
return "\(key!)=\(value!)"
}).joinWithSeparator("&")
request.HTTPBody = body.dataUsingEncoding(NSUTF8StringEncoding)
request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type")
var url: String?
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) {
data, response, error in
XCTAssertNil(error)
do {
let json: NSDictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments) as! NSDictionary
XCTAssertNotNil(json["url"])
url = json["url"] as? String
} catch {
XCTFail("failed to parse mock setup json")
}
expectation.fulfill()
}
task.resume()
self.waitForExpectationsWithTimeout(5, handler: nil)
XCTAssertNotEqual(url, "")
return url!
}
func testCall() {
let app = XCUIApplication()
app.launchArguments.append("MOCK_API")
app.launchEnvironment = [
"MOCK_API_URL": self.setupAPIMockWith([
"msg": [
"code": -99,
"text": "yoyo"
]
])
]
app.launch()
app.childrenMatchingType(.Window).elementBoundByIndex(0).childrenMatchingType(.Other).element.childrenMatchingType(.Other).elementBoundByIndex(1).childrenMatchingType(.Button).elementBoundByIndex(0).tap()
app.staticTexts["yoyo"].tap()
}
}