Latest news about Bitcoin and all cryptocurrencies. Your daily crypto news habit.
I worked closely with our tester last year to add the first UI tests target to our iOS project. We had some simple goals to measure our success:
- Writing easy to maintain tests.
- Abstract the app internal wiring and architecture from the tests.
- An easy mechanism to stub relevant API calls (find the relevant calls using Charles or Charles Proxy).
What are you actually testing
To make sure that you are testing your app and not your backend for example, it is a good idea to stub all your API calls. This also guarantees consistency, stable and less flaky UI tests.
Your aim should not be to test your backend or the integration between it and the frontend but rather to test the app in isolation and confirm that it works consistently based on some predetriment conditions.
Setting things up
If your app doesn’t already have a UI testing target, you will have to add one by going to File > New > Target > iOS UI Testing Bundle.
The first thing that trips most people is that you cannot stub your API calls in your XCTestCase, because it just opens your app so any stubbing you do there will be lost.
You have to use launch arguments instead and let your app knows that it is in UI test mode and what API calls need stubbing.
How to stub your API calls
When it comes to returning fake data (predetriment responses) to your app you have a bunch of options like mocking repositories (managers, etc.. based on your app architecture) protocols that should make API calls. In other words circumventing the call before it actually hit the network layer.
Another option is stubbing all the web calls that your app might make during a test. And with OHHTTPStubs your app actually execute all the code in your repository and when you hit the network layer OHHTTPStubs returns a predetriment response or an error. Unlike the first option this can actually expose hidden errors in your repository.
An easy structure to stub existing and future API calls
In my previous attempts to write maintainable UI tests mostly on Android, mocking repository methods has always been tricky specially from the tester point of view because they do not get to work on the codebase on daily basis. On the other hand you can easily run the app and find out all the requests going out of the app and their responses and easily stub them without having to worry about what is going on in the code.
I still wanted to make the code clear and somewhat easily mappable to our API structure/documentations. Any REST backend has different APIs and each API has a bunch of endpoints so that is exactly how the code looks like. I might even look at auto-generating all this code in the future.
protocol StubApi { }
protocol StubEndpoint {var path: String { get }var name: String { get }var type: String { get }var method: HttpMethod { get }var params: [String: String]? { get }}
extension StubEndpoint { // Helper serialization function.func toDictionary() -> [String: String] {var dictionary = [String: String]() dictionary["path"] = path dictionary["name"] = name dictionary["type"] = type dictionary["method"] = method.rawValueif let params = params {let json = try! JSONSerialization.data(withJSONObject: params, options: JSONSerialization.WritingOptions()) dictionary["params"] = String(data: json, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) }return dictionary }}
I also included the HTTP methods and resource types that the StubbingManager would support.
enum HttpMethod: String {case GET, POST, DELETE, PUTstatic func fromString(rawValue: String?) -> HttpMethod {guard let rawValue = rawValue else { fatalError() }switch rawValue {case HttpMethod.GET.rawValue:return .GETcase HttpMethod.POST.rawValue:return .POSTcase HttpMethod.DELETE.rawValue:return .DELETEcase HttpMethod.PUT.rawValue:return .PUTdefault: fatalError() } }}
enum ResourceType: String {case JSON}
An example API stub would look something like
// https://jsonplaceholder.typicode.com/class PostsStubApi: StubApi {class RetrieveOnePostItem: StubEndpoint {let path = "/posts/1" // This file name, it can exist anywhere but it needs to be accessible by the app target as well as the UI test target.let name = "stub_discarded_jobs"let type = ResourceType.json.rawValuelet method = HttpMethod.GETlet params: [String : String]? = nil}}
How to launch your app in UI test mode
Because you cannot stub your API calls in your tests, you will have to pass some data to your app when it is launched from a UI test. We can just use launchEnvironment on XCUIApplication for that.
class BaseTestCase: QuickSpec { // Or XCTestCaseprivate var stubManager: StubManager = StubManager() // Easily add a new stub before your test run.func stub(endpoint: StubEndpoint) { stubManager.add(stub: endpoint) }
// Remove all tests after a test has ran.func removeAllStubs() { stubManager.removeAllStubs(); }
// Here is where the magic happens.func launch(app: XCUIApplication) { app.launchEnvironment["stubs"] = stubManager.toJSON() app.launch() }}
And the StubManager can live in either the app or the UI tests target, it just has to be shared between both target.
import OHHTTPStubsfinal class StubManager {let jsonHeaders = ["content-type": "application/json"]private var stubs: [StubEndpoint] = []deinit { killStubs() }func add(stub: StubEndpoint) { stubs.append(stub) }func loadStubs() { // Just stub any image request to avoid any web calls. stub(condition: isExtension("png") || isExtension("jpg") || isExtension("gif")) { _ in let stubPath = OHPathForFile("stub.jpg", type(of: self))return fixture(filePath: stubPath!, headers: ["Content-Type" as NSObject:"image/jpeg" as AnyObject]) }
// Now let us go through the stubs array and apply them.for stub in stubs { // Base url for your endpoints.var condition = isHost("https://jsonplaceholder.typicode.com") condition = condition && isPath(stub.path)switch(stub.method) {case .GET: condition = condition && isMethodGET()case .POST: condition = condition && isMethodPOST()case .DELETE: condition = condition && isMethodDELETE()case .PUT: condition = condition && isMethodPUT() }if let params = stub.params { condition = condition && containsQueryParams(params) } stub(condition: condition) { _ in let bundle = Bundle(for: type(of: self))let path = bundle.path(forRe
Disclaimer
The views and opinions expressed in this article are solely those of the authors and do not reflect the views of Bitcoin Insider. Every investment and trading move involves risk - this is especially true for cryptocurrencies given their volatility. We strongly advise our readers to conduct their own research when making a decision.