At Oak City Labs, we rely heavily on unit testing in our quality software process. Unit testing is the safety net that lets us reliably improve existing code. Our testing suite double checks that modified code still behaves the way we expect. I’ve written before (here and here) about how we use dependency injection (DI) which makes unit testing easier. DI helps us wrap code we need to test in a special environment. By controlling the environment, we can make sure that the code being tested gives the correct output for a given set of conditions. This works very well for pieces of the application that we can encase in a layer that we control. Unfortunately, we can’t wrap everything.
Consider our API layer. This is the part of our application that talks to the server over the internet. It makes network calls, processes replies and handles errors like slow network responses or no network at all. In testing this code, we want it to behave as normally as possible for accurate testing, so we still want it to make API requests and interpret the results. At the same time, these are unit tests, so they should be fast and not depend on external resources. We don’t want to make requests to a real server on the internet. If that test failed, it wouldn’t be obvious if our code was broken, the server was down or the network cable was mistakenly unplugged. It’s important that our unit tests be self contained so when something fails, we know that a specific section of code has failed and we can fix it ASAP.
Back in the old days, before Swift, we wrote in Objective-C. Swift is a strongly typed language where Objective-C is weakly typed. While weak typing in Objective-C often gave a developer enough rope to hang themselves, it was flexible enough to do interesting things like easily mock pieces of software. Using mocks, fakes and stubs, you could (with some effort) replace pieces of the system software with substitute code that behaved differently. We could use this to test our API code by changing the way the system network routines worked. Instead of actually contacting a server on the internet, a certain call just might always return a canned response. Our API code wouldn’t change, but when it asked the system to contact the server for new data, our mocked code would run instead and just return the contents of a local file. By building a library of expected calls and prepared responses, we could create a controlled environment to test all our API code.
Swift, on the other hand, brought us strong typing, which wiped out whole classes of bugs and insured that objects were always the type we expected. We paid for this safety with the loss of the ability to easily mock, fake or stub things. Someday, Swift might gain new features that make this possible (maybe even easy) but for now, this is difficult to do with any efficiency. So, we need a new approach for testing Swift API code.
Like we said earlier, we don’t want to use external network resources to test our code because too many things are out of our control. But what if the test server were running on the development machine? In fact, what if the test server were running inside our application? Enter two open source projects — Ambassador and Embassy from the fine folks at Envoy.com. Embassy is a lightweight, dependency free web server written in Swift. Ambassador is a web framework that sits on top of Embassy and makes it easy to write a mock API server.
In this new approach to testing our API layer, we’ll write some Ambassador code to define our mock endpoints. We’ll declare what URL path they’ll respond to and what response the endpoint will return. Now, inside our unit test, we’ll fire up the mock server and run it on a background thread. Since you’re controlling both the client and server side of the request, you can make asserts on both ends of the exchange. On the server side, you can validate the data submitted by the client. On the client side, you can ensure that the response from the server was interpreted correctly. Ambassador has some nice conveniences as well for working with JSON data, introducing delayed responses from the server and other common testings needs.
In order to use our freshly built API mock server, all you need to do is change the server name used by the API layer. This is important because we don’t want to make significant changes to our code in order to test it. We want to test as close as we can to production code. By switching our base URL from “https://api.ourserver.com” to “http://localhost:8080”, we can test all our network requests with no external dependencies. Since we’re using dependency injection, this change is very simple to implement in our unit testing routines.
The move from Objective-C to Swift has allowed us to write cleaner and safer code, but the price we pay is the loss of the fast and loose, über permissive Objective-C runtime environment. Fast and loose always caused more problems than it solved, so I’m happy to see it go. A few of our existing solutions have gone with it, but with a bit of ingenuity, we can move forward with better and safer new solutions.