Pedro pin era cover

Network Testing

I am Pedro Piñera and I’m going to speak today about Network Testing.

Unit testing (0:30)

I will start by asking if you implement unit tests in your projects. Then you might be familiar with all of these libraries: Specta, Expecta, OCmock and OCmockito. If you use Swift, Quick and Nimble, they are also great approaches.

When we implement unit tests, what we are actually doing is testing our components. At the end, our apps are based on multiple components: view controller, clients, and features for the database. We test them like this: we have our component, we map the dependencies, given an input we expect something from the output side. However, our components can work as expected, but as we put them into a system the integration might fail.

This means that we also need to implement integration tests in our lab. We test our units of code, but we’ll have components isolated. We have a system, and we need to test that system. So instead of having these dependencies that we mock, we must test everything together as a system. We give the same input and we expect the output. If any of the dependencies don’t work as expected, we see an error and we have to fix it. We can determine that something is wrong with our system.

But, these components are used in viewcontrollers, and as you may know, viewcontrollers have a lifecycle. Who controls the lifecycle of these viewcontrollers? iOS? Maybe you’ll have some users there, tapping buttons, jumping from one view to the other. We will disappear, which means users are also part of our system. They do something with our app. Then we test it, like if they are part of our system.

Acceptance testing (2:23)

Implementing acceptance testing in your projects is worse than the first one.

For those who don’t know what UI testing is, or acceptance testing is, what we mean in this case is instead of just testing these components that are in the back inside of our apps, we take the view and we test entirely from the view. We test what the user would be doing with our app, and what we would expect from the viewer’s perspective.

We tap this button. We jump to this view. We tap this other button. Something happens. If it doesn’t, it breaks. It’s not expected. They rely on the UI layer, which means in an accessory, which means it relies on the UI layer. We are looking for accessibility components. We tap them.

Get more development news like this


let app = XCUIApplication()
app.launch()
app.buttons["Stream"].tap()
app.tables.staticTexts["Enrique Iglesias"].tap()

This an example, we have our app, we launch it, then we look for this string button. We tap it, and then we get Enrique Iglesias, and we look for the track of this artist. We test from the user’s point of view. However, there’s non-UI stuff taking place in our app. For example, networking.

Networking (3:53)

Suddenly we go into a view, we send out a query because we want to synchronize the tracks of this artist, or we want to send something to the analytics, not something taking place in background. We have two kinds of networking activities in our app. We have the activity, that is an activity that reflects on the UI.

We fed something. Then suddenly it blanks and refreshes. We have the data. We tap the element. But we have something in networking that doesn’t reflect on the UI. For example, analytics. The API interaction is something that we can test from the acceptance test.


let app = XCUIApplication()
app.launch()
app.buttons["Stream"].tap() //-----> REQUEST SENT
waitForContent(timeout: 5)
let enriqueCell = app.tables.staticTexts["Enrique Iglesias"]
enriqueCell.tap() // -----> REQUEST :+1:

Going back to the example that I showed you before: We launched the app. We tapped the stream. We wait for the content to be presented on the screen. We tapped the Enrique Iglesias track. And then, if everything’s fine, our test passes. But what about analytics?


let app = XCUIApplication()
app.launch()
app.buttons["Stream"].tap() //-----> SCREEN EVENT SENT
let enriqueCell = app.tables.staticTexts["Enrique Iglesias"]
enriqueCell.tap() // -----> WAS IT REALLY SENT?

In this example, I go into a stream view, I send an event saying, hey, the user opened this screen. I tapped, but how can I test if the event was actually sent? Because we don’t know it from the testing point of view.

What happens to the view if the event is not sent? For companies this is very critical for their business. What if we want to track purchase from the user? What if we want to track the activities that the user does into the app? This type of information helps design the product in the future.

What if the event is sent with the wrong parameters? How many of you have someone on your team coming to you and saying something like this? What can we do then? And that’s a problem. We have the app, it’s not behaving as expected, then suddenly, the product team is complaining about it. You have to fix it, send a new build, and wait until Apple approves this to get it in the store.

Szimpla (5:52)

As I was thinking about it, and thinking about with what’s on the cloud, we have a private library called MrLoggerLogger. We use it with Google Merchant. What we do is, whenever we run an acceptance test, we add a flag saying, hey, start recording on the request. It saves the request in a file, then when we execute this again, we take the JSON file and we try to match it with the request that we get the second time out of the same ones that we get at the beginning.

But, this is for Ruby, and I wanted to have something similar in Swift. I came up with this library. The library is facing two components. This is our app, and this is our test. When we start the application, we’d run a proxy, then every time we get that request, we save the request in memory, and we proxy the request to the internet. We don’t touch the request, we just send it.

From the other point, from the testing target, whenever we start testing something, we send an event on the HTTP server, saying hey, start recording everything that’s being delivered through the networking. This guy receives the request, and starts saving all the requests in memory. Whenever the test finishes, we say, hey, stop.

We notify the server, hey, stop, and give me the request. I get the request back, and then I send it into a disk. This is the first time. The second time, I have the request on the disk, so I repeat the same steps, but the second time I confirm this guy instead of recording, what I do is just validating, so I took the request from here, take the request I have prerecorded, I match them, and if they are the same, the test passes. It’s very simple. Only three steps. The first one, we record the data. I call it a snapshot because it’s was inspired in the Facebook lab.


class AppUITests: XCTestCase {


	func testDefault() {
		try! Szimpla.Client.instance.start()
		let app = XCUIApplication()
		app.buttons["Navigate"].tap()
		try! Szimpla.Client.instance.record(path: "CMDUConf.json")
	}
}

How can we record a snapshot? This our UI test from the Apple framework, so the first thing we do is notify the client. Hey, start recording the request. We do whatever we want to do with the app. Then we finally record the request in a JSON file. The second step is validating a snapshot.


[
	{
		"url": "https://api.soundcloud.com/tracks/1235123",
		"parameters": {},
		"body": {}
	}
]

This what we get in the JSON file. This is what I’m seeing right now. Probably in the future I will add more parameters like headers and some other information about the request. We have the URL, we get the parameters, and we also get the body if you are sending a post or a post request.


[
	{
		"url": "https://api.soundcloud.com/tracks/[0-9]+",
		"parameters": {},
		"body": {}
	}
]

Something interesting that I also implemented is, let’s say you are not interested in this particular track but you want to have more flexibility with this JSON file. Instead of using just the track identifier, you can use regular expressions, and it passes as well.

The third thing is update the test.


class AppUITests: XCTestCase {

	func testDefault() {
		try! Szimpla.Client.instance.start()
		let app = XCUIApplication()
		app.buttons["Navigate"].tap()
		try! Szimpla.Client.instance.validate(path: "CMDUConf.json")
	}
}

To stop recording you can see here that we change the record word with validate, and the rest is basically the same. We don’t change anything.

Demo (9:15)

If everything works, I’m going to show you a demo so you see how everything works in practice. I’m going to show you first the app, so you understand what the app is about. It’s a very, very simple app, it has two viewcontrollers, one of them pushes the other one.

This is the CMD+UConf application. It’s asking us if we’re going somewhere. Of course, we want to know Marcheta, which is kind of a party. And yeah, we want to go. Why not? So we go to the party. Let’s see how we can use our library with this very simple application.

The first thing I would like to show you is we need to send something to the networking. I go to the first viewcontroller and from here, I say, send this request. I’m going to send the event to analytics. This one append. For those who are wondering what this request is doing internally, this is just a top function level. I am proxying the configuration, because I need to proxy that request, so I create a configuration with this instance in between.

I know that for production this is something you shouldn’t do, so we can have a flag detecting if it’s in production, debug, or this is the bit for UI testing, and depending on that, we just want configuration. But in this case, I didn’t have this flag, so I used this configuration for always. I send the request from this side, I go to the second viewcontroller, where I want to track something similar.

In this case I want to track the screen, so I take this line, I paste it here, this is screen. Screen. I’m sending that request, so I’m going to go now to implement the UI test for this use case. Before going into that, I have to point out something. If I leave this in, I have to specify a library where the steps are going to be recorded. This is similar to what Facebook does. You define the variable, it’s saying, oh, save them in the product folder, test fixtors, and the snapshots are going to be there. I go to the UI test, which is here, and for only one test, called testMarcheta, and then I’m going to record it.

For those who are not familiar with UI testing, you can record everything. It opens a simulator, you are just tapping a button, and it creates everything for you. So I start recording it. And, that’s it. The only thing touched is just tapping one button.

If we didn’t use the networking thing, but we are going to use it. The first thing we do is import it because it’s not imported. Then I’m going to say, hey, start recording something. Then I launch the app. I’m going to have a small delay before tapping the button. The last thing I do is send in the request, That’s it.

This is the client side, this is what we do for the UI test, but in the app, remember, I have to launch the server. Again, we can use this variable, if it’s true, because it’s a UI test build, then we just launch it. It’s already being launched. So if everything goes fine, I should run the UI test and I should get that file in the fixtors folder. Fingers crossed. Okay, everything passed. There you have it. We have the first event, which is the event with the parameter Marcheta, the second one is the other screen.

If we go now back to the test, and we say, hey, instead of recording now, I don’t want to record anymore, but instead, just validate it. So I go here, I change the name. And I run it again. Since I didn’t change anything, and no one touched anything from the code, it should work. Then let’s say, from analytics there is a requirement which is, Marcheta should be false in this case. For any reason, or Loronos should be anything else, so I change the JSON file, and if I run it again, it should fail. It can be either your analytics team changes something from the JSON file, or any developer from the team goes to this component and goes, hey, I copied and pasted something and I forget one of the parameters. But you see it failed. It says, error validation, we are expecting false, but we got one instead.

This is the library. And we use it at SoundCloud because it’s pretty critical for us to make sure all the test passes. That all the tests are sent properly. This is very, very important for the company.

Snapshot approach (17:43)

As I mentioned, my snapshot approach was inspired by the Facebook library. It supports validators, which means, let’s say we want to use this for a validator that I provide with the library, but instead if you want to provide your own validator.


public protocol Validator {
	func validate(recordedRequests recordedRequests: [Request], localRequests: [Request]) throws
}

You can. When you record, when you validate the request, you can say, hey, use this validator instead, and what the validator is is just simple. It confirms a simple protocol that takes all the arrays with the requests that have been recorded, the ones that you have been looking at, and then you say, hey, throw an error if it doesn’t pass, or just don’t throw anything if it passes. It also supports custom filters.


public protocol RequestFilter {
	func include(request request: Request) -> Bool
}

For example, let’s say you launch the app, you are just jumping through different screens, you have a lot of requests, you send requests to your API, and you also send requests to analytics provider. What if you want to test only analytics? So you can provide one filter and say filter only the requests with this URL, or filter the requests only with this particular path. You can pass the filter, and it uses the filter to filter the request.

This is great for the analytics team because whenever they want to change something and make sure the developers are doing things properly they’ll have to go into your code. But they don’t like to do that and they just want to open a JSON file and touch something and make your test fail. This is something they can read, they can understand, they open the JSON, they create a commit, they push it, and they make your test fail.

Conclusion (19:42)

Some of my references, the first one is the library. I would like to point to the snapshot testing library. Then for the HTTP server I used this Swifter library, and all the images that I used for the presentation, I took them from Unsplash. Thank you very much.

Network Testing Resources

Next Up: New Features in Realm Obj-C & Swift

General link arrow white

About the content

This talk was delivered live in July 2016 at CMD+U Conference. The video was transcribed by Realm and is published here with the permission of the conference organizers.

Pedro Piñera

I’m Pedro Piñera Buendía, 🇪🇸 iOS Developer currently working at SoundCloud as Core iOS Engineer. I’m a passionate runner and traveller. I’m always willing to learn new things and become a better developer and person. Most of my spare time projects are Open Source libraries that I share on my GitHub account. You can check them out here. In my free time, I’ve also worked on projects like GitDo, an Issues management tool for GitHub, and some other apps with Caramba. We craft apps and tools for developers. We, as developers, have great tools to make this world better, why not using them?

4 design patterns for a RESTless mobile integration »

close