I recently decided to tackle a long-postponed refactoring of my source base: to move from my own, custom CFNetwork-based “URL request” class, to something based on the modern NSURLConnection or NSURLSession classes from Apple.
When I first started writing networking code for OS X, NSURLConnection didn’t exist yet. If you wanted a class that approximated the convenience of NSURLConnection as we know it today, you had to write your own wrappers around the lower-level CFNetwork HTTP library functions. If you needed to have precise control over the loading of network resources from the web, you simply had to use CFNetwork (or the 3rd-party open-source libcurl library, which some folks did do!.)
When I acquired MarsEdit from NewsGator in early 2007, I wasn’t surprised to learn that Brent Simmons (the original author) had also implemented his own CFNetwork-based HTTP class: RSHTTPDownloader. I took a look at it and my own class, RSHTTPRequest, and ended up migrating the best parts of his with the best parts of mine, keeping the name I had grown accustomed to: RSHTTPRequest. (It was a great convenience that Brent’s Ranchero Software and and my Red Sweater adopted the same class-name prefix).
Fun fact: I actually worked briefly on the CFNetwork team at Apple, and learned the ropes of that somewhat-daunting, low-level API. Suffice to say, I was less scared off by CFNetwork than some people, but even I didn’t consider it to be perfectly simple to use. However, it worked well enough and when NSURLConnection debuted later in 2007, I found it somewhat wanting. I can’t remember the details but there were little things that I’d come to appreciate about having direct access to the “streams” and I lost that by moving to NSURLConnection. So I stuck with RSHTTPRequest.
Fast-forward more than seven years and I’m still on RSHTTPRequest. But now, it’s clearly to my detriment. Not only has Apple been improving NSURLConnection and the rest of the networking stack all this time, a couple years ago they introduced the all new NSURLSession, which makes the whole setup easier to use and more powerful at the same time. Capping off the incentives is the fact that new functionality such as background network loading sessions are essentially required if you want to make good use of recent technologies such as Share Extensions. In short, it’s a great time to switch to NSURLSession.
Just Switch to NSURLSession!
The only problem is I’ve spent more than seven years building a pretty complex network-oriented app around this one, I hesitate to say little anymore, this one disgusting evolution of what started out as a simple wrapper around CFNetwork’s HTTP loading mechanism. RSHTTPRequest has every manner of convenience method, special cases, not to mention subclasses covering highly specialized networking operations such as POST’ing HTTP forms, communicating with most blogging APIs on the planet, etc. You get the idea: I can’t just switch to NSURLSession. So what am I going to do?
I’m a very paranoid person when it comes to code, and it soothes me to take small, predictable, measurably safe steps when it comes to situations like this. I already have a number of unit tests on RSHTTPRequest that verify its behavior to a reasonable extent. The basics are covered, and as I’ve discovered and addressed bugs over the years, I’ve usually taken the time to add unit tests covering the nuances. So to be perfectly clear, the main impediments to my switching to NSURLSession are:
- The interface to NSURLSession is totally different from my custom class.
- The default behavior of NSURLSession is totally different from my custom class.
So I could hunker down for a long weekend, write a new NSURLSession-based helper, RSSpiffyRequest, and then change every single line of code that deals with RSHTTPRequest to instead use the pertinent methods on the new class. One problem with this approach is that I have to come up with the all new RSSpiffyRequest interface from scratch, before I change a single line of code. The other problem is even if I come up with the perfect interface for my new class, I’ll inherit all the unpredictably different behaviors of NSURLSession. Things will randomly not work. Crashes will probably happen. It will suck.
The only way forward, to my paranoid mind, is to find a way to slow down the transition so that fewer parts are changing at once. Since RSHTTPRequest already satisfies a contract with countless clients, and because it’s already covered by a moderate number of unit tests, the safest way forward is to change RSHTTPRequest so that it’s based on NSURLSession.
But hold on, that’s too fast for me, too. I don’t want to reimplement RSHTTPRequest’s whole interface at once, hold my breath, and hope that all the code still works. Instead, I need a new class that implements the interface of RSHTTPRequest, can be tested like RSHTTPRequest, and can be moved to by client code gradually, as the expected behavior is confirmed.
Parallel Request Classes
The approach I decided to take is to add a new class, RSLegacyHTTPRequest. The name is future-proofed: the presence of the word “Legacy” in the name is to remind me that it’s not the end-all be-all NSURLSession subclass. It’s not the ideal, convenient networking object that should be used across my source base. Instead, it’s the nasty, pragmatic, full-of-history class interface of RSHTTPRequest that happens to be implemented on top of NSURLSession.
This gives me the freedom to willy-nilly switch client code from RSHTTPRequest to RSLegacyHTTPRequest, just to see what happens. A great place to start was in the RSHTTPRequest unit tests class I mentioned. It currently verifies the behavior of RSHTTPRequest for most all niggling network details I’ve encountered over seven years. I switched it from testing RSHTTPRequest to testing RSLegacyHTTPRequest by simply adding this to the top of the test file:
#define RSHTTPRequest RSLegacyHTTPRequest
Boom! Everything fails. That’s a nice start, and makes for an easy challenge to chip away at. Pick a failing unit test, and make it work. In my case this involved discovering how to simulate or approximate the interface of the old RSHTTPRequest, but on top of NSURLSession. Slowly but surely, I came to a point where I had a new RSLegacyHTTPRequest class, completely based on NSURLConnection, that passes all my existing tests.
I was far from done. I have other classes that use RSHTTPRequest, and themselves have unit tests of their own. I pulled a similar trick to uncover other unit testing edge cases that should have been in RSHTTPRequest, but weren’t. I was beefing up my test class, in some cases even finding residual bugs from my old implementation.
I got to a point today where I realized I had a more-or-less “fully” functional RSLegacyHTTPRequest class, but I am still not ready to abandon RSHTTPRequest. I have other clients to meander through, verifying that they behave as expected when switched over to the new class. In the mean time, I was stuck wondering whether the unit tests, those golden unit tests, should be left testing the old RSHTTPRequest or testing the new RSLegacyHTTPRequest? Answer: they’re both in use, so both should be tested.
Multi-Class Unit Tests
Finally we get to the namesake of this article’s title: how do I use the same precious collection of unit testing code but apply it to two (or more!) classes that happen to share the same interface? The answer is to subclass. When you implement and run unit tests in an Xcode project, the testing system looks at runtime for any classes in the test bundle that “have methods that start with -test”. What this means is that, if you wanted to, you could redundantly run all the unit tests in one of your test cases twice simply by subclassing it and leaving the subclass empty. If “MyTests2” is a subclass of “MyTests”, then at runtime, it’s going to find and execute all the tests in each of the classes, which will happen to be exactly the same tests.
For my situation, I want to run all the tests twice, but I want to run them on instances of different class. I decided to accomplish this by making some modest edits to the test class, replacing every alloc/init pair for RSHTTPRequest:
[[RSHTTPRequest alloc] init...]
With a bottleneck method that instantiates the class from an overridable Class object:
- (Class) classForHTTPRequestInstances
return [RSHTTPRequest class];
- (RSHTTPRequest*) requestForTestingWithURL:(NSURL*)theURL
return [[[self classForHTTPRequestInstances] alloc] init...];
I then added a new unit test class called “RSLegacyHTTPRequestTests”, which merely implements -classForHTTPRequestInstances to return RSHLegacyHTTPRequest. Now when I build and run unit tests, every single, painstakingly collected unit test gets run twice, once for each of the super-important URL request wrapper classes in my framework.
Over time, hopefully over a short period of time, I’ll move away from RSHTTPRequest to RSLegacyHTTPRequest. And then, hopefully over a similarly short period of time, I’ll start moving to RSSpiffyRequest. All the while, however, I’ll rest easy at night, knowing that to a very great extent, all the functionality I’ve come to expect over years of developing this code is being constantly confirmed by this glorious set of unit tests.