【问题标题】:XCode UI Testing swizzle API class methodsXCode UI 测试 swizzle API 类方法
【发布时间】:2015-11-10 12:21:35
【问题描述】:

我有一个带有两个按钮的简单应用程序,它调用 JSON Web 服务并打印出结果消息。

我想尝试新的 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"))
    }
}

现在我添加了一个 UITest 目标,记录了点击按钮,我有类似的东西:

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 返回 false 时包含一个测试,而不会弄乱 API。因此,我将ZZSomeAPI.swift添加到 UI Tests 目标并尝试了方法调配(UITest 代码已更新):

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。
是不是因为只有在 UITest 和 main 目标是两个不同的应用程序时才在测试目标上进行调配?
有没有办法解决这个问题?

我找到了Mock API Requests Xcode 7 Swift Automated UI Testing,但我不确定如何在此处使用launchArguments。
在示例中只有一种情况,但我需要模拟 call() 方法以获得不同测试方法的不同结果......如果我使用 launchArgument 例如 MOCK_API_RESPONSE 包含要返回的完整响应,主要目标应用程序委托将有一些“丑陋的仅测试”代码......有没有办法检查(在主目标中)它正在为 UITest 目标编译,所以它只包含那个模拟launchArguments的代码?

最干净的选择确实是开始工作......

【问题讨论】:

    标签: ios cocoa-touch xcode7 xcode-ui-testing


    【解决方案1】:

    Xcode UI 测试在与您的应用程序不同的应用程序中执行。因此,对测试运行应用程序中的类的更改不会影响被测应用程序中的类。

    这与单元测试不同,单元测试在应用程序进程中运行。

    【讨论】:

    • 关于如何干净地使用launchArguments而不影响公开发布中的代码的任何提示?类似 objC #IFDEF TESTENV if launchArgument... #ENDIF
    • 感谢您的确认,也许我会退回到单元测试而不是 UI,尽管能够在不调用远程 API(我无法控制)的情况下测试 UI 会很好)
    【解决方案2】:

    我已接受 Mats 的回答,因为实际问题(UI 测试中的混乱)已得到回答。

    但我最终得到的当前解决方案是使用在线模拟服务器http://mocky.io/,因为它的目标是模拟远程 API 调用。

    使用 API URL 的 public 属性更新了 ZZSomeAPI.swift,而不是在方法中获取它:

    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 测试用例类中创建 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 = $0.0.stringByAddingPercentEncodingWithAllowedCharacters(.URLHostAllowedCharacterSet())
                let value = $0.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()
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-12-13
      • 1970-01-01
      • 2015-10-14
      • 2016-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多