Add ///@mockolo to certain file of the API repository

Method 1: Make mockable annotation to certain files

  1. make bash file
#!/bin/bash
sed -i '' '12i\
/// @mockable' File.swift

12 is line number

2. edit Makefile

all: mockable

mockable:
	./mockable.bash

3.

$PROJECT_DIR/third_party/mockolo -s Pods/Backend -d Neo/Generated/MockAPI.swift

Method 2: Make mockable annotations to all files and that mockolo to read certain files

  1. Edit api.mustache file
/// @mockable you need to include the file to ios/scripts/generate-mock.bash to make Mock.
public protocol {{apiPrefix}}{{classname}}Type {

2.

$PROJECT_DIR/third_party/mockolo -srcs Pods/Backend/social/Classes/Swaggers/APIs/CertainAPI.swift -d [project]/Generated/MockAPI.swift

session.buyProduct doesn’t work in simulator after

try session.buyProduct(productIdentifier: “com.monthly”)

A test using this line worked as expected on device.

But on simulator, after a test case that uses the code below, that method doesn’t trigger updatedTransaction

store.startProductsRequest("com.monthly", IAPTicket())

So I changed “try session.buyProduct(productIdentifier: “com.monthly”)” to just “store.startProductsRequest(“com.monthly”, IAPTicket())”.

Maybe there is some problem of buyProduct method on simulator after purchase.

Set status code of mock response in unit test

MockURLProtocol.requestHandler = { request in
            let response = HTTPURLResponse(url: request.url!, statusCode: 400, httpVersion: nil, headerFields: nil)!
            return (response, Data())
        }

class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
    
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override public func stopLoading() {
    }

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            return
        }
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
}

class MockNetworkTestCase: XCTestCase {
    let mockNetworkManager = MockNetworkManager()

    override func setUpWithError() throws {
        NetworkManager.shared = mockNetworkManager
        
        let configuration = URLSessionConfiguration.default
        configuration.protocolClasses = [MockURLProtocol.self]
        NetworkManager.shared.af = AlamofireHelper().session(configuration: configuration)
    }
    
    override func tearDownWithError() throws {
        MockURLProtocol.requestHandler = nil
        mockNetworkManager.responseHandler = nil
    }

}

navigationController doesn’t deinit if it’s pushing a view controller

override func tearDownWithError() throws {
        try super.tearDownWithError()
        
        UIApplication.shared.keyWindow?.rootViewController = nil 
    }

func testNextStep_thirdPartyNewMemberWithEmail_showNameStep() {
     let navigationController = MockNavigationController(rootViewController: UIViewController())
     UIApplication.shared.keyWindow?.rootViewController = navigationController
}

Even though I set rootViewController as nil when tear down, navigationController doesn’t get deinit.

func testNextStep_thirdPartyNewMemberWithEmailAndName_hideNameHelperLabel() {
        // Given
        ...
        
        let navigationController = MockNavigationController(rootViewController: UIViewController())
        UIApplication.shared.keyWindow?.rootViewController = navigationController
        let expectation = XCTestExpectation(description: "push Login Stack View Controller")
        navigationController.pushCompletion = { viewController in
            expectation.fulfill()
            
            // Then
            ...
        }
        
        // When
        CreateSessionManager.shared.nextStep(...)
        wait(for: [expectation])
    }

class MockNavigationController: UINavigationController {
    var pushCompletion: ((UIViewController) -> Void)?

    override func pushViewController(_ viewController: UIViewController, animated: Bool) {
        if let pushCompletion = pushCompletion {
            pushCompletion(viewController)
        } else {
            super.pushViewController(viewController, animated: animated)
        }
    }
}

func nextStep(...) {
    UIViewController.current().navigationController?.pushViewController(LoginStackViewController())
}

So I changed pushViewController method not to really push when push a view controller for test. To setting rootviewcontroller as normal, when pushCompletion is not set, I made pushViewController method work as normal. Because in production code, there was a code to find top view controller of navigation controller of the window.

How to Mock URL Response in Test

class NetworkManager: NSObject {
    static var shared: NetworkManager = NetworkManager()

    var af: Session!
    
    override init() {
        super.init()
        let configuration = URLSessionConfiguration.default
        af = AlamofireHelper().session(configuration: configuration)
    }
...
}

class MockURLProtocol: URLProtocol {
    static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?
    
    override class func canInit(with request: URLRequest) -> Bool {
        return true
    }
    
    override class func canonicalRequest(for request: URLRequest) -> URLRequest {
        return request
    }
    
    override public func stopLoading() {
    }

    override func startLoading() {
        guard let handler = MockURLProtocol.requestHandler else {
            return
        }
        do {
            let (response, data) = try handler(request)
            client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
            client?.urlProtocol(self, didLoad: data)
            client?.urlProtocolDidFinishLoading(self)
        } catch {
            client?.urlProtocol(self, didFailWithError: error)
        }
    }
}

import Foundation
import Alamofire

class MockNetworkManager: NetworkManager {
    var responseHandler: ((URLRequest?) -> Void)?
    
    override func didFinishRequest(_ response: AFDataResponse<Any>, _ completionHandler: @escaping APICompletionHandler) {
        super.didFinishRequest(response, completionHandler)
        responseHandler?(response.request)
    }
}

class MockNetworkTestCase: XCTestCase {
    let mockNetworkManager = MockNetworkManager()

    override func setUpWithError() throws {
        NetworkManager.shared = mockNetworkManager
        
        let configuration = URLSessionConfiguration.default
        configuration.protocolClasses = [MockURLProtocol.self]
        NetworkManager.shared.af = AlamofireHelper().session(configuration: configuration)
    }
    
    override func tearDownWithError() throws {
        MockURLProtocol.requestHandler = nil
        mockNetworkManager.responseHandler = nil
    }

}

wait for KVOObservation

class PlayerViewControllerTests: XCTestCase {
    var observation: NSKeyValueObservation?
    
    override func tearDown() {
        observation = nil
    }
    
    func test...() throws {
        // Given
        ...

        let tutorialExpectation = XCTestExpectation(description: "tutorial")
        observation = TutorialManager.sharedInstance()?.observe(\.finishedAlert, changeHandler: { (tutorialManager, _) in
            tutorialExpectation.fulfill()
            
            // Then
            ...
        })
        
        // When
        ...
    }

I think it’s better to use block observation to get compile error if the value is not observable.

You have to change property using self (not _) in objective c to get observation.

Don’t forget to make observation nil in tearDown, if not the observation persists in other tests.