06

Apr

2017

Improving Swift Unit Tests with Objective-C

By: April 6, 2017

With Swift approaching its third anniversary of being announced to the world, it's no longer the immature adolescent that can be passed by for the old reliable language, Objective-C. Instead, it's now something that can't be ignored if you’re an iOS engineer. After all, it's the future direction of our platform.

But for many of us, we still find ourselves working with Objective-C for a variety of reasons. Most of us don't have the luxury of working entirely with a shiny new Swift code base. Most of us also have to support older apps that have a large amount of Objective-C code. 

If that sounds familiar, here’s a secret tip for you: You can write unit tests for Objective-C code with Swift. 

Yes, it’s possible! And I highly recommend it. 

Not only is it a low-risk way to practice your Swift coding skills, it also prevents you from creating new Objective-C code that will need to be migrated later. Even if you already have a unit test suite for a particular Objective-C class, you can still add a Swift XCTestCase subclass and new unit tests for that same Objective-C class.

An Unexpected Case for Swift

This approach particularly comes in handy when working with Objective-C code bases where you're forced to write new Objective-C to expand the functionality of the app in order to modify existing code. You can create a new Swift unit test for verifying the new functionality added in Objective-C.

If you're setting out to add Swift tests to a project that is entirely Objective-C based, there's one little quirk with Xcode 8 to work around. You need at least one Swift file with a bridging header in your main target for Xcode to allow you to import that target into your Swift unit tests as a module. Otherwise, you will get the error, "No such module," when attempting to run your Swift test.

A Problem You'll Encounter

After writing Swift for awhile, it's easy to to forget that back in the Objective-C world, each class generally had two files: a header file (.h) and an implementation file (.m).

Best practices suggested that unless something needed to be exposed to other classes, it should go within the .m file alone. This went for method definitions and property declarations. For example, consider this UIViewController:

viewDidLoad() sets two labels based on some values stored in NSUserDefaults. Lack of dependency injection aside, coming up with an approach for writing a unit test for this method is fairly straightforward and easy to implement in Objective-C:

Objective-C makes it really easy to get access to the private properties for nameLabel and emailLabel. All you have to do is use a private category defined within the sample .m file for the XCTestCase subclass.

Now, let's say you take my advice and try writing this unit test in Swift for the same Objective-C class. You'd probably start with something like this:

It's a pretty straightforward translation from the Objective-C test. The only problem? There's a compiler error:

Screen Shot 2017-03-23 at 3.53.04 PM

The compiler is telling you that it can't find the method definitions for nameLabel or emailLabel on ViewController. It's actually the same problem that you would have experienced in Objective-C if the trick of using a private category had not been in place.

Some testing enthusiasts advocate that all code is testable. However, you have to balance this with other design principles. For me, I'm not going to violate the protection of encapsulation by exposing those two UILabel properties through the header file – they don't need to be there.

So how do you fix it?

Solving The Problem with Objective-C

Objective-C to the rescue. You can solve this problem by using a similar trick to the one we originally used to get the Objective-C version of this test to work: a category. This time, though, it won't be a private category. Instead, it will be a test-target scoped Objective-C category that exposes the private properties from ViewController.

Here's how you do it:

Step 1: Create a new header file in the group for your unit tests and name it ViewController+Testing.h (the prefix should match the class under test; even though a suffix of +Testing isn't technically required, it helps provide a context to the purpose of the file):

Screen Shot 2017-03-23 at 4.01.08 PM Screen Shot 2017-03-23 at 4.01.21 PM

Step 2: Add a private category that exposes the private properties on the class you need to test:

Step 3: Import the header file from the bridging header for the test target:

And 💥, that's it! Go back and attempt to compile your Swift test where you are accessing those properties, and you'll see it works!

While this is just a simple example, this strategy can be used in any situation where you want to write a Swift test and access private methods or properties on an Objective-C class.

I wired all of this together into a final project where you can see how it works and try it for yourself here.

How About You?

Have you dipped your toes into the international waters and crossed the borders between Objective-C and Swift tests? What challenges have you encountered? How have you found it? Leave a comment and let me know!