HTTP testing in Swift with Nocilla
In this post you’ll learn about:
- testing networking layer with Nocilla and XCTest,
- using Objective-C libraries with Swift,
- new XCTest asynchronous testing API,
- CocoaPods,
- basics of test naming and structure.
Testing networking code
There are a few problems with unit testing the networking layer:
- Results are not 100% reliable (tests may fail due to a number of factors - bad connection, server downtime, etc.).
- Test are slow - any external dependencies (networking, disk access, etc.) cause tests to be slower, than when they would run solely in-memory.
- It may be also hard to setup and test specific conditions, especially if you want to check something other than the “happy path” (“Does this error handling code works like it’s supposed to?”).
Solution to this is to stub networking, to mitigate the need to hit the network or having a working server. Nocilla is a library that stubs HTTP requests and responses. It has great API with custom DSL that allows to fluently compose the stubs. It’s written in Objective-C, but due to Swift - Objective-C interoperability it’s possible to use it for tests in Swift projects. Easiest way to integrate a third party dependency in a project is with CocoaPods.
Installation Guide
As usage instructions on Nocilla’s github are written for Kiwi framework and for Objective-C, I decided to write a guide on how to set it up in Swift project and use with vanilla XCTest
framework.
Steps:
- Install CocoaPods.
- In projects top level folder execute command:
pod init
- It will create a
Podfile
. Edit it with your text editor of choice and add Nocilla to your test target. In my case test target is namedServiceTests
andPodfile
looks like this:
- It will create a
target 'ServicesTests' do
pod 'Nocilla'
end
- To install CocoaPods dependencies call
pod update
. After this projects have to be run from newly created(project name).xcworkspace
. - To use Objective-C code in Swift a bridging header is required. For any libraries you’d like to be visible in Swift add imports to this header.
- Simplest way of setting up bridging header is to add an empty Objective-C file to the target (New File -> Source). Xcode will ask you if you want a bridging header. Sure you do! After this you delete the just added empty file. This helps to avoid creating header manually (and potentially messing up paths).
- Add
#import "Nocilla.h"
to generated header - now it’s accesible in Swift.
Test suite setup and testing asynchronous code
I’ve created NocillaTestCase
(gist deriving from XCTestCase
, to encapsulate all the setup that’s needed to be done when testing with Nocilla. Class methods setUp
and tearDown
make sure the library is doing the stubbing only during this test suit:
override class func setUp() {
super.setUp()
LSNocilla.sharedInstance().start()
}
override class func tearDown() {
super.tearDown()
LSNocilla.sharedInstance().stop()
}
Stubs get cleared between tests of the suite.
override func tearDown() {
...
LSNocilla.sharedInstance().clearStubs()
}
Due to the fact that “NSURLSession
API is highly asynchronous” we need to use the new XCTest
API and setup an expectaction before each test. Expectaction signals that there’s async code to be executed. At the end of the test call waitForExpectationsWithTimeout
, so the function won’t return and execute next test. It waits until expectaction is fullfiled (you need to call it manualy in test completion handler) or if it times out. When running tests with Nocilla, timeout can be set to very low values (under 0.1). Even if it fails it’s still faster than running a network call - response is almost immediate.
Add it at the end of the test:
waitForExpectationsWithTimeout(0.1, handler: nil)
Passing nil
to handler is enough for tests to fail in case of timeout. If you need to do any additional work in handler, be sure to check if handlers NSError
parameter is non nil
and call XCTFail
assertion, as handler is invoked both on expectaction fullfilment and timeout.
As each test in a suite will use asynchronous API, NocillaTestCase
creates new expectaction in setUp
instance method and clean it up in the tearDown
. Note: to avoid creating initializer
1
expectaction is declared as an explicitly unwrapped XCTestExpectation!
.
Testing HTTP requests with Nocilla
After this whole setup (which more or less you’ll need to do only once) we’re ready to write first tests!
I’ll use Nocilla to test WebService
class I created to handle JSON services. First test will check if completion handler is passed right data when server responds with a proper JSON.
func test_fetchParameters_200ProperJSON_CallsCompletionHandlerWithJSONObject() {
// arrange
let service = makeJSONWebService(baseURLString: baseURLString, defaultParameters: emptyParameters)
stubRequest("GET", baseURLString).andReturn(200).withBody("{\"ok\":1}")
// act
service.fetch(emptyParameters) { result in
self.expectation.fulfill()
// assert
XCTAssertEqual(["ok": 1], result.data()!)
}
waitForExpectationsWithTimeout(0.1, handler: nil)
}
Stubbing is straighforward. Start with stubRequest
and define HTTP method and URL with either a string or a regex, then define request/response headers, payload, and response status code. In this example tested unit of work is a web service library fetch call. Scenario/state under test is 200 OK response from server with JSON payload. As a result we’re expecting completion handler to be called with JSON parsed to dictionary. Other scenarios to test could include handling of the 404 response:
stubRequest("GET", baseURLString).andReturn(404)
or testing malformed JSON data reponse:
stubRequest("GET", baseURLString).andReturn(200).withBody("{1}")
There are many more examples of stubbing requests with Nocilla’s elegant DSL on github. Those include even responding with data recorded with curl
and failing requests with specific NSError
.
Tests Structure and Naming
Notice the Arrange-Act-Assert pattern that helps to keep tests clean. In Arrange part all needed objects/dependencies are set up, then method under test is invoked (Act), and finally we assert results/the state of the system under test (Assert). More details about the pattern here. Another thing worth noting is the omition of the assertion message. I’ve used following standard for naming unit test (more details):
UnitOfWork_StateUnderTest_ExpectedBehavior
-
Which would be impossible anyway as XCTestCase default initializer takes in NSInvocation, which is "unavailable". ↩
</li> </ol></div>