Network Stubbing Options for XCTest and XCUITest in Swift
This post explores all the options available to test your iOS app's network layer in Swift, as well as network stubbing libraries to use with the XCTest framework.
Join the DZone community and get the full member experience.
Join For FreeEvery iOS application requires the data to be displayed in the app. Unfortunately, we can't put all the data inside our app. iOS developers have to make network requests to get this data from the internet and use it in the apps. Apple has provided various libraries and frameworks like URLSession and NSURLProtocol to deal with the networking layer and there are some third-party frameworks like Alamofire available to deal with network requests. However, testing of the asynchronous code in the form of network requests became very complex due to different approaches used whilst testing the network layer. In this detailed post, we will explore all the available options to test network layer in Swift and cover details of network stubbing libraries available to use with the XCTest framework.
Network Testing in Swift
Network testing of iOS apps written in Swift can be performed at the unit, integration, or UI level, depending on the project needs. The unit and integration tests are generally fast and stable; on other hand, UI tests are slow and brittle. From what I have been reading on the internet, people are doing network testing in iOS using various approaches. Some of the common approaches are as follows:
- Mocking classes with protocols in the unit or integration tests:
Mocking is fragile and hard, it gets harder when it gets to Swift. There are no mature mocking libraries available in Swift to generate mocks like it exist in Java, Ruby, Python or other languages. The developer has to write all the mocks by hand and tightly couple the test code with production code. There is a great article on network testing in iOS here to understand more about how to mock with protocols. This approach definitely makes the app more testable, but it involves writing lots and lots of protocols and mock classes. In Swift, we need to mock most of the classes and methods that we need to test. This is going to be a lot of work for the iOS developers and soon it becomes chaos.
- Recording and playing back network requests:
Swift has some libraries that allow us to record network requests and play them back to avoid the unit tests going through the networking layer. They store the recorded data in the files, and that data is reused instead of making network calls. The most popular library is DVR, which makes fake NSURLSession requests for iOS apps. There is one more library, Szimpla, which does a similar thing. You can watch the session on Szimpla here and the session on DVR here on Realm academy.
- Stubbing the network requests using libraries:
There is another approach to network layers, which is making the network call instead of mocking URLSession and returning a stubbed or static response instead of a real response. Using stubs, we can still achieve the goals of network testing as well as we don't have to tightly couple test code to production code. We will see details of the libraries that can be used for stubbing the network requests with Swift. Some of the most popular libraries are OHHTTPStubs, Mockingjay, and Hippolyte for XCTest (unit) testing.
- Interacting with the UI using XCUITest frameworks:
User interface tests go through the network layer and cover testing of all the aspects of the network. Apple has the XCUITest framework to cover Xcode UI testing. There are some libraries which we can use to stub the network for UITest. Some of the popular libraries are Swifter, SBTUITestTunnel, and Embassy for XCUITest (UITest).
All the approaches mentioned above have their own pros and cons so its essential to pick one that suits the project need. In this post, we will see how to stub out network layer for unit and user interface tests.
Git Location App
We will be using the GitHub API for this entire demo. We will make a network request to the URL https://api.github.com/users/shashikant86, get the location from the API, and display it in the app. You can see the JSON response by visiting this link in the browser. Our app has a button called "Make Network Request." Once pressed, it shows the location from the API or from stubbed data. Don't worry if you not following at this point- we will get the source code of the app with all the tests at the end of this post.
Stubbing Unit Tests (XCTest)
Before we dive into stubbing, let's see how we can test the GitHub location by making a real API call with the XCTest framework. Ideally, we can have an API Client in the app, pass a URL to the client, and assert that we get the right data, but I am a lazy developer and didn't make my code testable at all. In this case, we have to make the direct network call from the test. A typical test will look like this:
func testGitUserData() {
guard let gitUrl = URL(string: "https://api.github.com/users/shashikant86") else { return }
let promise = expectation(description: "Simple Request")
URLSession.shared.dataTask(with: gitUrl) { (data, response
, error) in
guard let data = data else { return }
do {
let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.mutableContainers)
if let result = json as? NSDictionary {
XCTAssertTrue(result["name"] as! String == "Shashikant")
XCTAssertTrue(result["location"] as! String == "London")
promise.fulfill()
}
} catch let err {
print("Err", err)
}
}.resume()
waitForExpectations(timeout: 5, handler: nil)
}
In this test, we are making a network call using URLSession and parsing the JSON response to check for name and location field. This test will pass, but it has a few drawbacks. The result of the test depends on the network; if the network went off, our test will fail. The test will be incredibly slow, as it makes a real network request and waits for the response. These kinds of tests would be very brittle, fragile, and unmaintainable. Ideally, we should be able to stub the response and make the test network dependent. Stubbing the network calls will allow us to test edge case scenarios. = Network requests can be easily stubbed at the unit level using the XCTest framework. This can be achieved with some of the open source libraries that we will be cover briefly.
Mockingjay
Mockingjay is a library which allows us to stub the network call and return the response we expect. It can be installed using Cocoapods. Carthage installation isn't mentioned in the README of this project, so it's a good idea to use Cocoapods. We can stub the URL to respond with the JSON data that we want, e.g. if we want the location to be Paris, then we can stub the response using Mockingjay, like this:
let body = [ "location": "Paris" ]
stub(uri("https://api.github.com/users/shashikant86"), json(body))
Now the GitHub API will respond with the location of Paris instead of London. We can assert that the location in the test above as XCTAssertTrue(result["location"] as! String == "Paris"). We can also create a JSON file (e.g. Feed.json) in the test target and stub the response from the static JSON file.
let path = Bundle(for: type(of: self)).path(forResource: "Feed", ofType: "json")!
let data = NSData(contentsOfFile: path)!
stub(uri("https://api.github.com/users/shashikant86"), jsonData(data as Data))
In this way, we can stub the API response using Mockingjay. At the time of writing this post, Mockingjay works for Swift 3.2 projects and has some issues with Swift 4 projects.
Hippolyte
Hippolyte is another stubbing library that is written in Swift to stub the request and response. We can install Hippolyte using either Cocoapods or Carthage. This is a much lighter and easier way to stub requests and responses. You then register this stub with Hippolyte and tell it to intercept network requests by calling the start()
method. In our example above, we can easily stub the data from the JSON file using the following code:
guard let gitUrl = URL(string: "https://api.github.com/users/shashikant86") else { return }
var stub = StubRequest(method: .GET, url: gitUrl)
var response = StubResponse()
let path = Bundle(for: type(of: self)).path(forResource: "Feed", ofType: "json")!
let data = NSData(contentsOfFile: path)!
let body = data
response.body = body as Data
stub.response = response
Hippolyte.shared.add(stubbedRequest: stub)
Hippolyte.shared.start()
Now the response to the Github API will use the stubbed calls from the Feed.json file and we can assert the location mentioned in this file. Hippolyte is lightweight and can be easily used for stubbing requests and responses, but it might not have advanced features that you are after. Check out the full features offered by Hippolyte on GitHub here.
OHHTTPStubs
The OHHTTPStubs framework has been around since the Objective-C days. This framework was being used to stub the Objective-C stubs for iOS apps. Although the roots of OHHTTPStubs are in Objective-C, it has a wrapper that works for Swift as well. We have to install OHHTTPStubs using Cocoapods and have to install both the Objective-C and Swift version in the Podfile, as follows:
pod 'OHHTTPStubs/Swift'
pod 'OHHTTPStubs'
In our example above, we can stub the request and return the response from the static file using OHHTTPStubs:
stub(isPath("https://api.github.com/users/shashikant86")) { request in
let stubPath = OHPathForFile("Feed.json", type(of: self))
return fixture(filePath: stubPath!, headers: ["Content-Type":"application/json"])
}
Now the response to the Git API will use the stubbed calls from the Feed.json file and we can assert the location mentioned in this file. OHHTTPStubs is quite a mature library with a full feature set, but it was originally designed for Objective-C.
Stubbing UI Tests (XCUITest)
Apple announced UI Testing support in Xcode at WWDC 2015. UI tests are designed to be completely black box without having any access to the main application code and data. XCUITest uses two standalone processes, Test Runner and Target App. The test runner launches the target app and uses the accessibility capabilities built into UIKit to interact with the app running in a separate process. While mocking or stubbing UITest, we have a few options:
- Mock and pass launch arguments/environments:
This means you can not mock the API directly. The only way to communicate with the proxy app is to pass the Launch Arguments or Launch Environments. We can pass mock the API and create a launch environment variable and pass it to XCUITests, but it requires a lot of hard work. The code is surrounded by if-else statements to determine the build configuration, which doesn't sound great.
- Mock the backend with a web server and return responses:
The other option remaining is to stub the network calls and get the desired data back from the server. In order to do this, we need to mock the backend and point our app to use those endpoints. The question is, can we still use stub services mentioned above for unit tests. We can still use those libraries to stub out the network requests, but sometimes it requires a change in the logic of the main app and needs to refactor things, which adds extra work. We need to add a lot of test code in the production code.
The good approach would be to run a web server locally or remotely to control the data that we need. Also, it should require fewer changes in the application code. There are several ways we can achieve stubbing the network requests for UI tests. Let's consider each of them briefly. You may notice that libraries available to stub UI tests are completely different than those we used for unit tests, as additional work is required to get the libraries working with UITests.
Swifter
Swifter is an HTTP engine written in Swift, which currently works in sync style, but the next version is planned to be in async style. The Swifter approach to stub UI tests was originally published in this article in detail, so I should not duplicate that, but in summary, Swifter does the following:
- Runs the server inside the test process so that we don't need to explicitly start or stop the server.
- Completely isolated from the app code and everything stays inside the UI tests target.
- CI server integration doesn't need additional setup.
- We can use initial stubs as well load dynamic stubs during test execution.
The only thing we need to do is point our app to the localhost by changing the Info.plist file. We can add the following code to enable local networking:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
<!--To continue to work for iOS 9 -->
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Swifter is available to download using Cocoapods, Carthage, and Swift Package Manager. Again, all the instructions to setup XCUITests are in the blog post here.
Embassy
This is another great approach, followed with Envoy to stub network request for XCUITests. They have written a great blog post about it. Basically, they created a couple of libraries to deal with network request stubbing for UI Tests. The libraries are Embassy and Ambassador. You need to read this article to get you started with Embassy, which works for the latest Swift version as well. In summary, Embassy's approach is:
- Lightweight and asynchronous event loop-based style.
- A bit of setup code we need to write/copy to get going with the tests.
- Define the router and provide your stubbed response.
SBTUITestTunnel
SBTUITestTunnel is another library which can be used to stub network requests, as well as some other things like interact with NSUserDefaults, download/upload files from/to the app's sandbox, monitor network calls, and define custom blocks of code executed in the application target. There is an article on how to use SBTUITestTunnel for network stubbing, plus the README file with a setup and usage guide. In summary, SBTUITestTunnel needs the following things:
- SBTUITestTunnel needs server and client libraries installed. The server needs to be installed on the main target and the client needs to be installed on the UITest target.
- In the main target, we need to change the build configuration and add pre-processor macro. In the AppDelegate, we need to take off the server for the debug or test configuration.
- In the test itself, we can stub the network call to get the desired response.
Server Side Swift Frameworks: Vapor
The Server Side Swift frameworks are not production-ready yet, but we can use them for the purpose of stubbing XCUITest. I haven't read about anybody doing that on the internet, but it works perfectly. There are a few Server Side Swift frameworks available in the market, like Perfect, Kitura, Zewo, and Vapor, but for this demo, we will use Vapor. I will probably write a detailed article on this topic, but for now, let's talk about it briefly.
Get the Vapor framework using Swift Package Manager, as mentioned here. We need to tell Vapor to return the response we want when specific endpoints are called inside the main.swift file:
import Vapor
let drop = try Droplet()
drop.get("users/shashikant86") { request in
var json = JSON()
try json.set("location", "Vapor")
return json
}
try drop.run()
Now that we told the server to return the location as Vapor when API endpoints are called, we can build the server and start it using this command:
$ swift build
$ .build/debug/Server serve
Now, you should see the server running on port 8080, serving the desired response. We can write a test as usual, passing launch environment pointing to the localhost.
Non-Swifty Web Servers
There are still some people who prefer to run web servers which are written in other crazy programming languages like Java, Ruby, NodeJS, etc. There are various options for running non-Swifty web servers, and the most common are:
- Sinatra: Ruby-based web server
- Mock Server: Available in various languages
- Wire Mock: Java-based mocking server
- NodeJS: JavaScript server
Enough Talk! Show Me the Code
At this point, if you are still reading, you might be wondering: this guy talked a lot about various libraries, but where is the f---ing code? As promised earlier, there is code for everything we've talked about and I have tried each and every framework mentioned above, apart from the non-Swifty web servers. The source code is available on GitHub.
Shashikant86/SwiftStub-XCTest
The SwiftyStub project has a GitHub location app, as mentioned above and covered both unit and UI tests using all the libraries mentioned above. The app has the environmental variable BASEURL passed to the scheme so that we can override it with localhost whilst UI testing. Some of the libraries aren't compatible with Swift 4 so this project is built using Swift 3.2.
You can try it on your own. Just clone the repo:
$ git clone https://github.com/Shashikant86/SwiftyStub-XCTest.git
$ cd SwiftyStub-XCTest
$ open open SwiftStubs.xcworkspace
In the unit test target SwiftStubTest, there are stubbed unit tests using Mockingjay, Hippolyte, and OHHTTPStubs. Also, there is a unit test making a real network request. In the UI test target SwiftStubUITest, we have UITest for stubbed networks using Swifter, Embassy, and SBTUITestTunnel and real network tests. You can execute each test using the Xcode diamond icon or by pressing 'CMD+U' to execute the entire test suite.
There is a UI test using Vapor, which might fail, which is expected. In order to execute this test, we need to build the server and start the service:
$ cd SwiftStubsUITests/Server/
$ swift build
$ .build/debug/Server serve
Now, we can execute the Vapor UI test inside Xcode. Please take time to go through the test and let me know if any improvements are needed.
Which One to Pick?
The selection of the right library depends on the skills of the engineers on the team, and the project itself. The important factor to check is the Swift version compatibility.
Unit Testing
If the team has used OHHTTPStubs from the old Objective-C, days then its worth it to carry on using it for Swift projects, as well. If you just need the response stubbing, then something lightweight like Hippolyte will do the job. Mockingjay is also a good library, but at the time of writing this post, it wasn't working as expected with Swift 4, so it's worth checking before using the library.
UI Testing
I would not recommend the approach of running non-Swifty servers written in Java or another crazy language, as it would be chaos if it stops working for an unknown reason. Swifter wasn't compatible with Swift 4 at the time of writing this post, whilst a pull request was open on Github. The Swifter approach looks painless as the server starts and stops as part of the test process, however, it works in sync style. Other approaches like Embassy and SBTUITestTunnel are worth giving a try, also; monitor the GitHub repo and see if they are still being maintained. Finally, if you are super-duper brave, then go to Server Side Swift frameworks. They are a bit heavy for this job, but they will work for sure!
Conclusion
Testing of the network layer is hard in Swift, as it requires a lot of protocol mocking and dependency injection code all over the application. Using lightweight third-party libraries to stub the network requests makes it much simpler by making UI and unit tests independent of the network and giving you full control over the data. What are your experiences using XCTest with stubbed data? Have I missed good libraries? Which approach have you used or liked from this post? Please mention them in the comments.
Published at DZone with permission of Shashikant Jagtap, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments