3

I have a simple app with two buttons that calls a JSON web service and prints out the result message.

I wanted to try the new XCode 7 UI Testing but I can't understand how to mock the API requests.

For simplicity I've built an example without actual requests nor any async operations.

I have the ZZSomeAPI.swift file in the main target:

import Foundation
public class ZZSomeAPI: NSObject {
  public class func call(parameter:String) -> Bool {
      return true
  }
}

Then my ZZSomeClientViewController.swift:

import UIKit
class ZZSomeClientViewController: UIViewController {
    @IBAction func buttonClick(sender: AnyObject) {
        print(ZZSomeAPI.call("A"))
    }
}

Now I've added a UITest target, recorded tapping the button and I have something like:

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()
    }        
}

So this works and running the test will print out true. But I want to include a test when the API returns false without messing the API. So, I added ZZSomeAPI.swiftto UI Tests target and tried method swizzling (UITest code updated):

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
    }
}

So, testSwizzle() passes which means swizzling worked. But testCall() still prints true instead of false.
Is it because the swizzling is only done on the test target when UITest and main target are two different applications?
Is there any way around this?

I've found Mock API Requests Xcode 7 Swift Automated UI Testing but I'm not sure how to use launchArguments here.
In the example there's only one case, but I need to mock the call() method for different results for different test methods... If I use a launchArgument such as MOCK_API_RESPONSE containing the full response to be returned, the main target app delegate will have some "ugly test-only" code... Is there any way to check (in the main target) that it is being compiled for a UITest target so it only includes that code for that mocking launchArguments?

The cleanest option would be indeed to get swizzling to work...

Community
  • 1
  • 1
Filipe Pina
  • 2,201
  • 23
  • 35

2 Answers2

3

The Xcode UI tests execute in a separate application from your application. So changes to classes in the test runner application will not affect the classes in the tested application.

This is different from a unit test, where your tests run inside your application process.

Mats
  • 8,528
  • 1
  • 29
  • 35
  • 1
    any hints on how to cleanly use launchArguments without affecting the code in the public release? Something like objC #IFDEF TESTENV if launchArgument... #ENDIF – Filipe Pina Nov 10 '15 at 13:18
  • and thanks for the confirmation, maybe I'll just fall back to unit testing instead of UI though it would be nice to be able to test the UI without calling the remote API (which I do not control) – Filipe Pina Nov 10 '15 at 13:20
2

I've accepted Mats answer as the actual question (swizzling in UI testing) was answered.

But the current solution I ended up with was using an online mock server, http://mocky.io/, as the swizzling goal was to mock remote API calls.

Updated ZZSomeAPI.swift with a public property for the API URL instead of getting it inside the method:

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 ...
  }
}

Then updated app delegate to make use of 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
    }
}

Then created setupAPIMockWith in my UI test case class to create mock responses in mocky.io on-demand:

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()
    }

}
Filipe Pina
  • 2,201
  • 23
  • 35