With great power comes greater responsibility. Unit testing superpowers in Swift help write better, more expressive code, but can be tough to get the hang of. In this talk Jorge Ortiz introduces unit testing and explains three scenarios of unit testing with Swift, providing you with practical advice for each of them. He also explores the implications of the recently introduced Swift 2.0.
Introduction (0:00)
My name is Jorge Ortiz. I work as a freelance consultant on architecture, unit testing, and Core Data. I also enjoy teaching bootcamps for Mobile Makers. To guide my discussion of unit testing, I draw on my experience as an instructor, both in the content I teach and the comments I receive from students. In this talk, I bring up three different unit testing scenarios which will hopefully make life a little better: @testable
, error handling, and test coverage.
The testing scenarios presented here are available on GitHub.
@testable (1:21)
@testable
solves the pains of testing methods from previous Swift versions. The need for @testable
stems from a key difference between Swift and Objective-C: access levels. In Objective-C, there are no access levels. Instead, things are either private or public. (Really, everything in Objective-C is public, because you can access everything in a class if you really want to.) When Swift was developed, I don’t think they were thinking about this problem since it wasn’t problematic in Objective-C. However, since Swift has proper access levels, testability does become a problem.
The confusion is this: your code is packed into a module, which can be either your app or a framework. The module can access anything in your code that is public or without declaration (i.e. internal). However, if you want to run a test, you are forced to use a different module. This test module will run what you have created, but when it tries to access a property, a class, or a method in your app, it won’t have access because those things are internal to your original module.
Instead, you have to use @testable
. Import your module as @testable
, and suddenly it will have access to everything that is internal. It’s much better than what we had in Swift 1. Let’s see how it works by testing out the initialization of an evil dude.
Check out the code for our simple EvilDude class. This class is for an evil organization which looks for evil recruits, called “Evil Talent.” If I want to hire an evil dude, I care about three things: his name, his destructive power, and his motivation. (We want to hire people people who are not only very destructive, but also well-motivated!) I’m using a failable initializer here, which will take data from a dictionary. This is meant to consume data from a JSON object that has been downloaded from the web. We will try and test this class. In order to do this, I’m going to create a simple new class, the unit testing class, called EvilDudeTests
. Check it out here in the project.
I will run the first test, testEvilDudeIsntInitialicedFromEmptyDict()
, which calls the failable constructor with some data. It will try to get a nil, because the dictionary is empty. Since it doesn’t have any data, it can’t create an object - we want to test that. I will also get a red flag from the analyzer saying that it doesn’t have access to this EvilDude
class, and doesn’t know what it is. This is because it is in a different module, the EvilTalentSwift2
module. This complaint may seem like a phantom, but in this case it is right – it really doesn’t have access to the internal class. I need to import it, by adding:
@testable import EvilTalentSwift2
I use the @testable
pattern. This import statement now basically says, “I want to import this module, but please let me have access to all of its internal properties, methods, and classes.” The red flag will disappear, and we can run this and pass the test. All we did was access is the class definition, but we haven’t yet accessed any property or any method of the class. We will access those in this next example, also included here in the repo.
Here, we create JSON data in order to create the object “Juanita Banana.” It will have a dictionary, a destructive power of 4, and a motivation of 2. I also have to write two statements for the creation of the object EvilDude
, dictionary
and jsonData
. Notice that I have also written another test, which checks that it returns a proper object if I pass the proper data:
func testEvilDudeIsInitialicedFromProperDict() {
XCTAssertNotNil(sut, "An EvilDude object must be returned from a dictionary with proper data.")
}
This will also succeed. Not only can I do that, but I can also define some constants in order to make my test more robust. Further tests will check not only access to the object itself, but to the properties. If we run this, we will have access to all of them, and hopefully, our tests will succeed.
Before, we had to make everything public, or worse, include the test classes in the testing target. That was detestable. 😂
Now, we have something much better. By simply using the keyword @testable
, we can import a module that provides access to everything internal.
Error Handling (12:36)
You already know that Swift 2 provides stricter error handling, which was something that was really missing in Objective-C. To deal with errors in Objective-C, we had to create a pointer to a reference, pass the pointer to the reference to the method that was going to create that error, then pass this error on to the method that was calling it. On top of that, every example carried with it the admonishment, “Please, do proper error handling.” I dare you to find any real example on the Internet that does proper error handling. There are not many, especially for small projects.
People are always looking at the bright side, executing the happy path, and thinking everything is going to be fine. My recommendation is: DON’T! This is very easy with Swift 2.0, because it provides mechanisms for proper error handling. Here is a short example:
class EvilTalentAPIClient {
let endPoint = "https://api.eviltalent.nowhere/v1/evildudes"
func fetchEvilDudes(completion: (jsonData: [NSDictionary]) -> Void) {
guard let URL = NSURL(string: endPoint) else { return }
let task = NSURLSession.sharedSession().dataTaskWithURL(URL) { (data, response, error) -> Void in
guard error == nil && data != nil else {
print("Failed to download data from the site.", appendNewline: true)
return
}
do {
if case let dictionaries as [NSDictionary] = try NSJSONSerialization.JSONObjectWithData(data!, options: []) {
completion(jsonData: dictionaries)
}
} catch {
print("Unexpected data format provided by server.", appendNewLine: true)
}
}
task.resume()
}
}
I have created a second class here, which is also part of my smart model. This class is called EvilTalentAPIClient
, and it’s going to grab data from a URL. It uses NSURLSession
in order to grab JSON data, which it then parses into objects, into a dictionary, and it passes that into a completion closure, shown as completion(jsonData: dictionaries)
here. I already have a couple instances of proper error handling. I also have a guard statement at the beginning of the method saying, “Okay, if I can’t create a proper URL from the string I’m using, then please leave this method,” since it makes no sense to make a connection to a URL that is not ready.
When I run the session, I have two conditions: the error is nil, or the data is not empty. You may be thinking, “How can I have no error but still have empty data?” If you are connecting to a server that returns a 500 error, you probably want to check that the connection is right, but you are still not getting the proper data because there is a server problem. You need to check both. If either of these are not right, you should not continue because it makes no sense to try to convert that into a dictionary or to call the completion method.
In order to test this, my first step is to try to make this easier, because you don’t want to test something this convoluted. I will create this method that is exactly the same as the one on top of it:
func parseServerData(data: NSData?, response: NSURLResponse?, error: NSError?) {
guard error == nil && data != nil else {
print("Failed to download data from the site.", appendNewline: true)
return
}
do {
if case let dictionaries as [NSDictionary] = try NSJSONSerialization.JSONObjectWithData(data!, options: []) {
completion?(jsonData: dictionaries)
}
} catch {
print("Unexpected data format provided by server.", appendNewLine: true)
}
...
Notice that the only thing wrong here is that the closure is able to capture context, while our method is not, because it’s created out of context. It’s defined as something in the class. How do we solve this? We have to create a property that will keep reference to this completion so we can use it later on:
// MARK: - Properties
var completion: ((jsonData: [NSDictionary]) -> Void)?
Since we have defined this as an optional, I can make this statement proper. I could substitute this parseServerData
with the closure that I have there, and it would do mostly the same thing. So, let’s make this a call that can be easily tested.
...
let task = NSURLSession.sharedSession().dataTaskWithURL(URL, completionHandler: parseServerData)
task.resume()
}
Much easier. Now, I would like to test that if I have wrong data, this JSON method is not going to be called. Because it makes no sense, as we agreed before. How do we deal with that? We are going to create a completion method in our test, a fake one, that is going to tell us whether it has been called or not. And then, we will use this as a parameter, as a property of this class, so it will tell us whether it has been called or not. Here is the test we created:
func testParseServerDataDoesntCallCompletionWithBrokenJSON() {
let brokenJsonData = "{".dataUsingEncoding(NSUTF8StringEncoding)
sut.completion = fakeCompletion
sut.parseServerData(brokenJsonData, response: nil, error: nil)
XCTAssertFalse(completionInvoked, "Completion closure must not be called with broken JSON data.")
}
Also, I create this fake completion method which uses a Boolean property to say, “Yes, I’ve been called,” or “No, I haven’t:”
func fakeCompletion(jsonData: [NSDictionary]) -> Void {
completionInvoked = true
}
I have also created a property for the system under test that we are going to run. Now, I have everything ready in order to test this method. I will know whether it is following the happy path or not. If something fails, this method shouldn’t be called. Basically, I am saying, “I am passing you some wrong data, and if you fail to parse this as a dictionary, then the completion shouldn’t happen.” If I call parseServerData
, I can claim that this is not going to run. Hopefully we’ll be able to test the sad path as well as the happy one.
func testParseServerDataCallsCompletionWithProperJSON() {
let goodJsonData = "[ {\"name\": \"Juanita Banana\"} ]".dataUsingEncoding(NSUTF8StringEncoding)
sut.completion = fakeCompletion
sut.parseServerData(goodJsonData, response: nil, error: nil)
XCTAssertTrue(completionInvoked, "Completion closure must not be called with proper JSON data.")
}
This is the test for when we pass a proper dictionary and we expect the completion block to be called. We create a dictionary that has some data in it. Then we call parseServerData
, and we check whether this has been successfully called or not.
Before Swift 2.0, we had to have pointers to a reference, then put the data there so we could model the behavior. Even when I taught this to my bootcamp students, it was quite a tough sell; it seemed like there simply had to be a better, less painful way to do it.
Now, with Swift 2.0, it is a proper structure. We can catch thrown errors, the flow is very clear, and everything is an object (or at least, it conforms to a protocol), which is what we love to have.
Test Coverage (22:59)
We write tests to provide us with assurance about how well our code behaves according to our expectations. We want to be sure that even though we have changed something here or there, the behavior is consistent with our prior expectations. We want to write tests because they are very useful for refactoring, but also because we are chasing phantoms. Often, we look for those red marks from the analyzer, but we don’t know whether they are right or wrong, because the tools are not yet mature enough. We love Swift, but the tools just aren’t quite there yet. If unit testing is important in any language, it is even MORE important in Swift because it provides you with at least some security about the code that you write. Test coverage helps contribute to understanding these factors, but is still fairly new.
Test coverage can be enabled in “Edit Scheme” -> “Test Debug” -> “Gather coverage data”. In Tests, I can see information about every class after running the tests once, in addition to information about every method in the class, which is even better. However, our parseServerData
is not fully tested.
On the right code border, Xcode will show a small red rectangle indicating that there are no tests for some pieces of the data. In order to fully test parseServerData
, you want to make a small change to make your guard statements clear. I would like to have two guard statements: one for the error, and one for the data.
guard error == nil else {
print("ERROR: Unable to connect to server: \(error!.localizedDescription)", appendNewline: true)
return
}
guard let data = data else {
// Obtain information from response
print("Failed to download data from server.", appendNewline: true)
return
}
Running this is even worse, because now it will tell me that I don’t have any tests for these two blocks of code. When I try to make these testable, I will notice that I have a dependency on try NSJSONSerialization.JSONObjectWithData(data!, options: [])
— an implicit dependency. I don’t have any control over that object or that method. One way to solve this is to create a method that will do exactly what the implicit dependency does.
do {
if case let dictionaries as [NSDictionary] = try NSJSONSerialization.JSONObjectWithData(data!, options: []) {
completion?(jsonData: dictionaries)
}
let dictionaries = try parseJSONData(data, options: []) as! [NSDictionary]
completion?(jsonData: dictionaries)
} catch {
print("Unexpected data format provided by server.", appendNewLine: true)
}
}
func parseJSONData(data: NSData, options opt: NSJSONReadingOptions) throws -> AnyObject {
return try NSJSONSerialization.JSONObjectWithData(data, options: opt)
}
}
Now we have that as a method of our class. Then, we can create a sub-class, mock it, and override the behavior of that method. In the test, I create the fake class, the mocking class.
// MARK: - Mock class
class MockEvilTalentAPIClient: EvilTalentAPIClient {
var parseJSONDataInvoked = false
override func parseJSONData(data: NSData, options opt: NSJSONReadingOptions) throws -> AnyObject {
parseJSONDataInvoked = true
return []
}
}
This mocking class is a subclass of the class that we are testing, the EvilTalentAPIClient
, but it only overrides the behavior of the method that we cannot control, because we do want to control that behavior. We want to know whether this method is called when we fail to meet either of the guard criteria (if the error is nil, or if there is no data). In the test class, I wrote a test that can check that over here.
These are the two tests that I have written for that. The first one says, “Test that parseServerData
exits if called with an error.” So, if I pass an error in, then I shouldn’t be calling the method for converting the data into a dictionary. It uses my mock class, which is a superclass of the one in order to override this behavior, and it will tell me, “Has this method been called? If so, then that’s wrong, because it shouldn’t have been called.” The same thing happens with the second test here. If we go back to test coverage, we will see that our parseServerData
method is now completed because we are covering every aspect of that method.
Finally, if you want to test let task = NSURLSession.sharedSession().dataTaskWithURL(URL, completionHandler: parseServerData)
, I would recommend you use something like lazy var sharedSession = NSURLSession.sharedSession()
, a lazy variable that will be created only if you don’t provide one. So, when you are testing, you will provide one value, like the mock class, but if you are not testing, then it will create the default one. Additionally, you can modify NSURLSession.sharedSession()
to make it testable, by invoking the property sharedSession
instead. So, let task = sharedSession.dataTaskWithURL(URL, completionHandler: parseServerData)
is testable, while the previous statement was not testable.
Before Swift 2.0, we had no knowledge about what parts of our code were properly covered by tests, and what parts of our code were doing great because we had enough tests to cover them.
Now, we have a list of which methods and classes have proper testing in place. Yay for test coverage 🎉
Caution: Use Your Head (31:39)
While these new insights are useful, please do not use them as a key performance indicator. Certainly, these are important measurements, but they are not the absolute truth. You don’t have to cover every single case in the world. True, it is better if you cover most of them, but if you’re doing TDD, you will get everything covered by default. If you are doing just regular testing after you write the code, there are some cases that are not as test-worthy as others. Use your brain to distinguish which is which, and write good quality code. All sample code from this demo is up on GitHub.
Q&A (32:30)
Q: Does Xcode’s code coverage tool also work with outside testing frameworks such as Quick and Nimble?
Jorge: That sounds unlikely, but it could be possible. I haven’t tested the code coverage tool with those; it would depend on the framework’s structure.
Receive news and updates from Realm straight to your inbox