How We Ensure Long-Term Quality for iOS Apps (Series 3/3)

Jonathan Arnal
LumApps Experts
Published in
8 min readSep 6, 2019

Introduction

This article is the last of three in the miniseries from the LumApps iOS team, in which we discuss the quality and maintainability of our mobile app.

We will focus now on how we implemented a stub system, allowing to return static data for each API call.

You will see a lot of stub or mock keywords in this article. Before any further reading, let’s take a look at what they mean.

Definition of mocks, fakes, and stubs are inconsistent and different across programming literature.

According to this article by Martin Fowler:

  • Stubs: provide canned answers to calls made during the test, usually not responding at all to anything outside what’s programmed in for the test.
  • Mocks: objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

Now that we have some background, let’s dive into the subject.

Why do you need stubs?

Snapshots

Like we saw in the previous article, we use fastlane to automate redundant tasks.
One of those tasks is to take AppStore screenshots for each app version.
We need 6 screenshots, in 6 supported languages for 3 different devices, giving a total of 108 screenshots.

It’s a time-consuming task, especially using real network and API calls that lead to potential latency and network failures.

UI Testing

Credit: http://mobileautomation.blogspot.com

Apple defines UI testing as:

Making sure that your app’s user interface behaves correctly when expected actions are performed.

At LumApps, we use UI testing to fill the gap left by unit testing for some parts of the app (like view controllers).

It allows us to be confident when shipping. We test all critical paths to ensure there is no regression even after a new feature or refactoring.

But, to allow a UI test to be valuable and consistent, we need predictable input data to feed the views.

To do so, we have two solutions:

  1. Create the context on server side:
  • Can take a lot of time to configure
  • Can be deleted with an update or by another user
  • Subject to network errors
  • Difficult to test failures or empty states

2. Return static data:

  • Manipulating only json files
  • Fast response
  • Consistent context tracked in version-control system (git)

UI Integration

Sometimes, when a new feature needs to be implemented, APIs are not ready at the time we start implementing them.
But one thing we always have is the data model that will be returned.

We could wait for the backend to be ready, but by returning static data with the correct format, we can work faster and more easily.

At this point, it clearly appears that we needed a way to stub data for specific API calls. Let’s see how we did it.

Adopted solution

Targets

First of all, we need to talk about Xcodetargets.

A target specifies a product to build, and contains the instructions for building the product from a set of files in a project or workspace.
Within Xcode, files added to a project, without being added to a target, have no effect on size and build time.

Thereby, we created a new target in LumApps project named LumAppsMock.
It contains all source code of production target (LumApps), but the opposite is not true.
Therefore we can add target specific logic, json files or other source code files without interfering with production code.

Thanks to this, we have different behaviors when launching the application:

  • Production: launch app
  • Mock: configure stubs and launch app

Let’s move forward and see how we configure our stubs.

Man-In-The-Middle (MITM)

Credit: https://www.imperva.com

In cryptography and computer security, a man-in-the-middle attack (MITM) is an attack where the attacker secretly relays and possibly alters the communications between two parties who believe they are directly communicating with each other. (source Wikipedia)

Here, the purpose is not to attack, but simply to intercept each API call made by the app and return a stub.

To do it, we used OHHTTPStubs library - let’s talk about it.

OHHTTPStubs

Basically, OHHTTPStubs can intercept all URLSession tasks and return a stub, instead of letting the request reach the server.

To intercept a request, you need to specify the conditions to match. It can be:

  • host (www.somehost.com)
  • path (api/user/details)
  • method (GET/POST/PUT/DELETE)
  • body and url parameters

When conditions match for a specific request, OHHTTPStubs can return:

  • Files: json, images, audio
  • Data: allows to build a response from an Encodableobject for example
  • Errors: simulate down network or return custom errors

Now, let’s see a concrete example of the implementation.

Implementation

We will see the pieces that allowed us to implement the solution.

TestingLaunchState

The first piece is TestingLaunchState, it’s a testing scenario that contains a specific set of API calls to intercept:

enum TestingLaunchState: String {
case debug
case externalActions
case screenShots
}

StubEndpoint

StubEndpoint contains all data needed to match a request, and information about the expected result when request has been matched:

struct StubEndpoint {

var bodyComparingMode: StubBodyComparingMode
var httpBody: Data?
var method: APIMethod
var name: String
var path: String
var queryComparingMode: StubQueryComparingMode
var queryParameters: [String: String]?
var resultType: StubEndpointResultType
var stubFileExtension: StubFileExtensionType

func stubFileName(suffix: StubFileSuffixType) -> String{
return name.appending(“_\(suffix.rawValue)”)
}
}

StubEndpointResultType

StubEndpointResultType defines return types for matched API requests:

enum StubEndpointResultType {
case file(StubFileSuffixType)
case empty
case error(StubEndpointErrorType)
}

Stubable

Stubable is a protocol defining an endpoint as capable to be “stubed”:

protocol Stubable {
var stubName: String { get }
var stubFileExtension: StubFileExtensionType { get }
}

Wrapping up

Endpoints

UserAPIFetchUser allows to log in a specific user:

final class UserAPIFetchUser {

init(email: String, password: String) {…}

var path: String {
return “/user/login/”
}

var method: APIMethod {
return .post
}

var httpBody: Data? {
let bodyParameters = [“email”: email, “password”: password]
return try? JSONSerialization.data(
withJSONObject: bodyParameters
)
}
}

UserAPIFetchUserProfilePicture allows to retrieve a specific user profile picture:

final class UserAPIFetchUserProfilePicture {

let userId: User.Identifier

init(user: User.Identifier) {…}

var path: String {
let userID = user.rawValue
return “/user/\(userId)/profilepicture”
}

var method: APIMethod {
return .get
}
}

Mock target extensions

In mock target, we define UserAPIFetchUserand UserAPIFetchUserProfilePictureas Stubable:

extension UserAPIFetchUserProfilePicture: Stubable {

var stubName: String {
return “userprofile”
}

var stubFileExtension: StubFileExtensionType {
return .jpg
}

static var mockUIAll: UserAPIFetchUserProfilePicture {
let identifier = User.Identifier(rawValue: “[0–9]+”)
return UserAPIFetchUserProfilePicture(user: identifier)
}
}extension UserAPIFetchUser: Stubable {

var stubName: String {
return “fetchUser”
}

var stubFileExtension: StubFileExtensionType {
return .json
}
static var mockUIFake: UserAPIFetchUser {
return UserAPIFetchUser(
email: “fake@fake.com”,
password: “fake_password”
)
}
}

Configure endpoints to mock

Now, lets define the stubs we need to install for .debug TestingLaunchState:

extension TestingLaunchState {var stubs: [StubEndpoint] {
switch self {
case .debug:
return [
StubEndpoint(
with: UserAPIFetchUser.mockUIFake,
expecting: .file(.success)
),
StubEndpoint(
with: StreamAPIList.mockUIBasic,
expecting: .file(.success)
),
StubEndpoint(
with: UserAPIFetchUserProfilePicture.mockUIAll,
expecting: .file(.success)
)
]
}
}
}

Under the hood

Let’s explain what is happening here:

StubEndpoint(
with: UserAPIFetchUser.mockUIFake,
expecting: .file(.success)
)

Condition:

  • path is the same as defined in UserAPIFetchUser (/user/login/)
  • method is the same as defined in UserAPIFetchUser (.post)
  • body parameters are the same as defined in mockUIFake(email: “fake@fake.com”, password: “fake_password”)

Stub:

  • return file named fetchUser suffixed with success, with type json => fetchUser_success.json
StubEndpoint(
with: UserAPIFetchUserProfilePicture.mockUIAll,
expecting: .file(.success)
)

Condition:

  • path is the same as defined in UserAPIFetchUserProfilePicture (/user/[0–9]+/profilepicture)
  • method is the same as defined in UserAPIFetchUserProfilePicture (.get)

Stub:

  • return file named userProfile suffixed with success, with type jpg => userProfile_success.jpg

Last step

At this point, we have defined some endpoints as Stubable, and we have created a debugcontext where those endpoints will be intercepted to return a specific stub.

The last thing we need to do is to use OHHTTPStubs to allow calls to be intercepted. To do so, we need to tell the app in which TestingLaunchStateit has to be launched.

This is done using launchArguments, because when using UI tests it’s the only way to communicate with the app:

Now, the only thing we need to do is to stub all API calls using OHHTTPStubs:

private func configure(with state: TestingLaunchState) {
state.stubs.forEach { (stub) in
let condition = getCondition(for: stub)
installStub(for: stub, with: condition)
}
}
private func installStub(
for stubEndpoint: StubEndpoint,
with condition: @escaping OHHTTPStubsTestBlock
) {
//This is where OHHTTPStubs is called
stub(condition: condition) { _ in
return
self.getResponse(for: stubEndpoint)
}
}private func getResponse(
for stubEndpoint: StubEndpoint
) -> OHHTTPStubsResponse {
switch stubEndpoint.resultType {
case .empty:
return OHHTTPStubsResponse(
data: Data(),
statusCode: 200,
headers: nil
)
case .error(let type):
return OHHTTPStubsResponse(error: type.error)
case .file(let suffixType):
let bundle = Bundle(for: type(of: self))
let resource = stubEndpoint.stubFileName(withSuffix: suffixType)

let path = bundle.path(
forResource: resource,
ofType: stubEndpoint.stubFileExtension.rawValue
)
return OHHTTPStubsResponse(
fileAtPath: path!,
statusCode: 200,
headers: stubEndpoint.stubFileExtension.contentType
)
}
}

Thanks to this, the three API calls that we’ve configured will return stubs instead of returning the data available on API.

What’s next?

At the moment, we need to retrieve the static data manually. We use Postman to make API calls and need to copy/past the result into a json file.

We could use SWHttpTrafficRecorder or build a custom tool to do it because this it not optimal.

For another improvement, we are discussing making our stub system open source to let the community improve it and use it. Let us know if you think it’s a good idea.

Conclusion

Thanks to stub system, the team has a lot of flexibility to work with different types of data. It’s very powerful and allows us to test all kind of possible results for a given API, even the edge cases we almost never receive.

We have reached the end of the miniseries concerning quality and maintainability on LumApps iOS mobile app.

Here is the summary of what we use:

  • Clean architecture to have a clean and comprehensive code base
  • Unit test and UI tests to be confident and avoid any regression
  • Continuous integration to avoid human errors and save time on recurring tasks
  • Mock target to control data when needed and allow tests to run in a reliable state

If you have any question regarding the article, feel free to ask and above all thanks for reading.

Jonathan Arnal, iOS developer at LumApps

--

--