Once upon a time we talked about how to initialize and launch view controllers manually. We did that so we could inject our dependencies via the initializer, then unit test our view controllers independently of those components.
Then we talked about how you can inject dependencies while loading your view controllers from the storyboard, instead of manually instantiating them.
We isolate the system under test from its dependencies by using faked versions of those dependencies.
Today, we’ll look at a fake in more detail for unit testing your iOS app.
Step 1: Test
This test shows you what the calls to our mock are going to look like. In this case, we pass in a FakeExampleService to our ExampleViewController for testing. We need our fake to be able to help us verify two things:
- The view controller makes the correct calls to the service.
- When the service returns a specific result, our view controller does the correct thing with that result.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
import Hamcrest | |
import FutureKit | |
@testable import MyExampleApp | |
class ExampleViewControllerTest: XCTestCase { | |
var fakeExampleService: FakeExampleService! | |
override func setUp() { | |
super.setUp() | |
fakeExampleService = FakeExampleService() | |
} | |
func testServiceCallInExampleViewController() { | |
let stubbedResponse = MyExampleAppResponse(name: "Johnny Appleseed", favoriteFruit: "apples") | |
fakeExampleService.stubbedResponse = stubbedResponse | |
let controller = ExampleViewController.loadFromStoryboard(fakeExampleService: fakeExampleService) | |
assertThat(controller.view, present()) | |
controller.searchBar.text = "Appleseed" | |
controller.didTapButtonToCallService(NSObject()) | |
assertThat(fakeExampleService.lastRequest, presentAnd(equalTo("Appleseed"))) | |
assertThat(controller.nameField.text, presentAnd(equalTo("Johnny Appleseed"))) | |
assertThat(controller.favoriteFruitField.text, presentAnd(equalTo("apples"))) | |
} | |
… | |
} |
Take a look at how we accomplish those two goals with our fake.
- We can obtain the lastRequest from our fake, which allows us to examine what the view controller sent in.
- We can stub the response from our fake, so it can mimic any situation we want to test that the real system might see in prod.
So how do we implement those things in our fake?
Step 2: Fake
You’ll see that we have two fields in our fake; lastRequest and stubbedResponse.
We override the method that the real system will call on the service to save off the last request, so we can check the contents of that request inside our test.
We also can set the stubbedResponse before that overridden method is called on the fake. This way, when the fake gets called, it can spit out the stubbedResponse that we gave it at the beginning of our test.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import XCTest | |
import Hamcrest | |
import FutureKit | |
@testable import MyExampleApp | |
class ExampleViewControllerTest: XCTestCase { | |
… | |
class FakeExampleService: ExampleService { | |
var stubbedResponse: MyExampleAppResponse? | |
var lastRequest: String? | |
override func getResponse(request: String) -> Future<MyExampleAppResponse> { | |
self.lastRequest = request | |
if let stubbedResponse = self.stubbedResponse { | |
return Future(success: stubbedResponse) | |
} | |
return Promise<MyExampleAppResponse>().future | |
} | |
} | |
} |
A quick aside: why am I calling this thing a fake, instead of a mock, stub, spy, or dummy? Martin Fowler famously coined these definitions of five different types of test doubles, but the descriptions become a bit nebulous in practice. Or, more often, the class of test double we’re working with changes over the course of development. First we don’t use it for much and it’s just a dummy, but as it takes on more sophisticated behavior we need it to have more behavior, as a stub or a mock. Then maybe we need it to actually implement something—a fake. Sometimes in systems not well-designed for unit testing, to get at something we even have to move to a spy and call the real methods, then check what happened after the fact.
What we have here is a subclass that overrides superclass methods and expresses behavior different from the real superclass, so I call it a fake. It’s worth noting that our fake’s simplified override does nothing more than a mock might do—takes method calls and spits out a predefined result. So, you could make an argument to call our fake a mock, and if I were using Mockito (as I can in Java) that’s what I would do.
This is where software engineering as part of a team plays a role in my decision-making. Swift doesn’t have a library as handy as Mockito, so I implement test doubles manually. When I’m rolling my own test doubles on a project with other programmers, I want the other programmers to know what I have done. Why? Because a hand-rolled fake checked by one developer (me) is more likely to produce unexpected behavior than a mock generated by a library used and bug-tested by thousands of developers, like Mockito. So if a test is failing for mysterious reasons, I want other programmers will see ‘fake’ and think ‘alternative behavior to the real thing,’ then check the fake for the cause of the problem. If they’re normally Java developers and they see ‘Mock’ they might think ‘this does the two things that mocks do and no more,’ assume they know exactly what the behavior looks like, and continue to beat their heads against a wall when the error is in the mock.
If you have trouble remembering the five kinds of test doubles, try memorizing them in order of the scope of their functionality:
- dummy (not called and doesn’t need to do anything)
- stub (takes method calls)
- mock (takes method calls and spits out results)
- fake (implements a simplified version of the real object’s methods)
- spy (calls methods on the real object and checks what happened after the fact)
Now back to our specific example of test-driving a behavior using a fake. These next steps don’t directly relate to the fake, but they’re necessary for injecting the fake into the view controller we want to test.
Step 2: Give the storyboard an identifier
You need this to refer to the view controller when you call loadFromStoryboard().
Step 3: Create an App Environment
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
class AppEnvironment: NSObject { | |
static let sharedEnvironment = AppEnvironment() | |
let exampleService: ExampleService | |
override init() { | |
self.exampleService = ExampleService() | |
super.init() | |
} | |
} |
This will inject the real objects into your real view controller when the init(coder:) method gets called on the real thing (this does not happen in your test setup).
Step 4: Implement!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class ExampleViewController: UIViewController { | |
class func loadFromStoryboard(exampleService: ExampleService) -> ExampleViewController { | |
let exampleViewController:ExampleViewController = | |
UIStoryboard(name:"Main", bundle:NSBundle(forClass:self)) | |
.instantiateViewControllerWithIdentifier("ExampleViewController") | |
as! ExampleViewController | |
exampleViewController.exampleService = exampleService | |
return exampleViewController | |
} | |
required init?(coder aDecoder: NSCoder) { | |
self.exampleService = AppEnvironment.sharedEnvironment.exampleService | |
super.init(coder:aDecoder) | |
} | |
} |
This is the fun bit. The next step would be to include an IBAction called didTapButtonToCallService. That will call your ExampleService that takes in a string, makes a search request based on that string, and returns a response object. This post is not about implementing such a service, but rather about test doubles and how to make and use them in iOS development. So I leave implementation of the service as an exercise for you. But, if done correctly, your ExampleService will make this test pass!
[…] Test-Driven iOS: Mocking Dependencies […]