In this CMD+U Conference talk, Jorge Ortiz discusses Test Driven Development (TDD) and performs a set of unit tests for a Table View Controller in a live demo.
Agenda (00:00)
My name is Jorge Ortiz-Fuentes. I do training and consultancy. I do training for companies, and I do training for my own, which is Canonical Examples. Today I will be speaking about Test Driven Development (TDD).
Please repeat after me: “I know how to write unit tests”. But usually we don’t write as many unit tests as we would like to: “My name is Jorge, and I don’t always write unit tests for my code.”
Probably you’ve been told that unit testing is mostly aimed to test the Model. And I don’t agree. Today we’ll discuss TDD and do a set of unit tests for a Table View Controller from scratch.
I’m going to explain architecture, because it will help us to do the proper unit testing of the View Controller. Most people have learned by using Model View Controller (MVC). Let me introduce you to Model View Presenter (MVP). You change “Controller” to “Presenter”, and that’s it.
But the truth is that the View is dumb, and it is receiving messages from the Presenter to tell it to display this or to change the color. Every time it gets an event, it passes the event to the Presenter - it is the Presenter that makes the decisions and communicates with the Model.
Here’s what we will test: we will have a Table View Controller. The Table View Controller is going to send an event to the Presenter, saying that the View has been created (it can fetch the data, for example). And it will retrieve information from the Presenter in order to display the number of rows and to configure each of the rows.
Do you write your test FIRST or afterwards? By first I don’t mean “Fast, Isolated, Repeatable, Self-verifiable, and Timely” (which is the way you should write tests, whether you use TDD or not). Writing a test before you have anything to test provides a better suited infrastructure.
TDD (05:13)
The “TDD dance” starts by choosing the functionality that we’re going to implement:
- We write a test that will fail, since we haven’t implemented the functionality yet.
- We write the simplest code required to pass the test.
- Without changing the functionality, we improve the code. We can move things, simplify things, reorder, change the namings.
…and we loop through these steps until we are done.
Roy Osherove gave this succinct explanation of how to implement the simplest code:
- The simplest code that you can write is the hard coded answer to what you’re expecting.
- Then, the one that is closer to the beginning of the scope, usually the beginning of the method.
- The one that is less indented,
- or the one that is shorter.
If you cannot get a hard coded one, you go with the other options until you get the actual solution to the test. And that’s it.
Live Demo (06:51)
VCTDD on GitHub is a simple replacement for the View Controller that comes with the Single View template of a project for Xcode (I just changed the name). I have configured the storyboard to have this GeniusesTableViewController
(this is going to include geniuses for evil stuff). This is a subclass of UITableViewController
.
import UIKit
class GeniusesTableViewController: UITableViewController {
static let ID = "GeniusesTableViewControllerID"
var presenter: GeniusesListPresenter?
override func viewDidLoad() {
super.viewDidLoad()
setUpForDynamicHeightCells()
presenter?.viewCreated()
}
func setUpForDynamicHeightCells() {
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 56.0
}
override func tableView(tableView: UITableView, numberOfRowsInSection: Int) -> Int {
return presenter?.numberOfGeniuses() ?? 0
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("GeniusTableViewCellID", forIndexPath: indexPath) as! GeniusTableViewCell
presenter?.configure(cell: cell, forRow: indexPath.row)
return cell
}
}
First, we create a template to test this. Notice that I have a couple of tabs: one is for the source code, and it has folders for the AppDelegate, the Views, the Presenter, the Model, and support files. In the Tests tab, I will have the same structure but for testing code.
The test case is going to implement the test suite for GeniusesTableViewController
. It’s asking me if I want to create an Objective-C bridging header, I say yes, and I move the bridging header file to the “support files” folder.
This is the full template that I created to use for testing. It implements what I like to have in a template: the parameters and constants; the variables that I’m going to use in the testing; the setup and the teardown. It’s created with the default initializer, the class that I want to test, and it is released because you want to have a fresh one for each of the tests that you write.
Obviously, I have a basic test that proves that at least the initial initialization will run properly. And I’m going to build and run this initial version of the template.
import XCTest
@testable import VCTDD
class GeniusesTableViewControllerTests: XCTestCase {
// MARK: - Parameters & Constants.
// MARK: - Test variables.
var sut: GeniusesTableViewController!
// MARK: - Set up and tear down.
override func setUp() {
super.setUp()
createSut()
}
func createSut() {
sut = GeniusesTableViewController()
}
override func tearDown() {
releaseSut()
super.tearDown()
}
func releaseSut() {
sut = nil
}
// MARK: - Basic test.
func testSutIsntNil() {
XCTAssertNotNil(sut, "Sut must not be nil.")
}
// MARK: - Stubs & Mocks.
}
But I don’t want to have the View Controller as it is created with the default initializer, because usually I want to obtain data from what I have defined in the storyboard. After passing the test I will make some changes to have a test that retrieves an initialized version of the View Controller from the storyboard instead of creating it with the default initializer.
The green checkmark shows me that the test has passed, so I’m going to change the code now. Let’s go to the beginning of the View Controller (the one that was created with the template).
class GeniusesTableViewController: UITableViewController {
static let ID = "GeniusesTableViewControllerID"
var presenter: GeniusesListPresenter?
override func viewDidLoad() {
super.viewDidLoad()
setUpForDynamicHeightCells()
presenter?.viewCreated()
}
func setUpForDynamicHeightCells() {
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 56.0
}
Here I will define a static ID that I will be using in order to retrieve the instance of the View Controller from the storyboard. In the storyboard ID, I paste the same ID that I have created.
In the tests, I will define a constant for the name of the storyboard (in case I’m using more than one, I prefer to have it as a constant).
class GeniusesTableViewControllerTests: XCTestCase {
// MARK: - Parameters & Constants.
let storyboardName = "Main"
let numberOfGeniusesOne = 7
let numberOfGeniusesAnother = 3
Next I will replace the instantiation of the View Controller with this one:
func createSut() {
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
sut = storyboard.instantiateViewControllerWithIdentifier(GeniusesTableViewController.ID) as! GeniusesTableViewController
}
And as you can see, it retrieves an instance of the storyboard and instantiates the View Controller with the identifier that I have provided, and it casts the View Controller to the type that I want to use. Now I have the View Controller as I want it obtained from the storyboard.
The next thing I want to test is event handling.
First, I will use a template. I’m going to name this testViewDidLoadInvokesCreatedViewEvent()
, which is telling us what it’s going to test. But, as you can see in my template, I have the three parts of a unit test: arrange, act, and assert. I’m going to replace the arrange by setting a Presenter to the View Controller. I know that I don’t have a Presenter yet, I know that this Presenter has not been defined yet, and I’m going to solve that in a moment.
// MARK: - Event handling in the presenter
func testViewDidLoadInvokesViewCreatedEvent() {
sut.presenter = presenter
_ = sut.view
// Assert
XCTAssertTrue(presenter.isViewCreatedInvoked)
}
In the View Controller, I create a variable, a property that will be the Presenter. And notice that now this code is complaining because I haven’t defined the class GeniusesListPresenter
- this is what I’m going to do next. I go to the Presenters, I create a new class, I call the new class GeniusesListPresenter
, and that’s it (thanks to Xcode for these rich templates).
Now I’m going to create the minimum that is required to have a class. That’s what I need in order to have this variable in the Presenter, but that is not enough. We also need the property. It is going to be a mock, a test double that we’re going to use in order to capture this event - the event that our View Controller tells our Presenter (which doesn’t exist yet) about the event that we care.
Notice that the property has the same name as the class - the type of this property is GeniusesListPresenterMock
. Not exactly the same, but a mock of the GeniusesListPresenter
, which doesn’t exist yet. I go to the section of stubs & mocks, and I will create the PresenterModel instance. It is a class, which is a subclass of the original one (the one that we will use in the future but we haven’t used yet).
// MARK: - Stubs & Mocks.
class GeniusesListPresenterMock:
GeniusesListPresenter {
}
Now we create the instance by using the default initializer for GeniusesListPresenterMock
. And we release the instance of presenter
afterwards; every time we get into a new test we will have a new Presenter mock.
func createSut() {
presenter = GeniusesListPresenterMock()
let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
ut = storyboard.instantiateViewControllerWithIdentifier(GeniusesTableViewController.ID) as! GeniusesTableViewController
}
func releaseSut() {
sut = nil
presenter = nil
}
Let me go back to the test. We have created a property for the Presenter in the View Controller, we have created a Mock. The next thing that I do is act. Since I want to load a View, I get an instance of the View Controller by itself - that’s the ViewDidLoad
part of it. And, finally, I will change the assert. I will assert that the Presenter has some property that tells me that the thing that it wanted to happen, has happened, which is XCTAssertTrue(presenter.isViewCreatedInvoked)
.
Let’s go to the real Presenter and add the declaration of the method that we are going to use, the event that we want to fire. This is going to be the method viewCreated
; the one that we want our View Controller to call.
Let’s go back to the mock and use this same method (overriding it).
// MARK: - Stubs & Mocks.
class GeniusesListPresenterMock: GeniusesListPresenter {
var isViewCreatedInvoked = false
override func viewCreated() {
isViewCreatedInvoked = true
}
}
}
I have created a variable isViewCreatedInvoked
that is a flag that will tell me when this mock has the method viewCreated
called. And we should be able to run this code and see that it fails, because we’re doing TDD. We have written the full set of things that we need for the initial test, and the code hopefully will fail.
Now we go to the class that we are actually writing, and in the viewDidLoad
method we write the invocation to the method that we wanted to check was happening. And we run the test again. Now I’m able to verify that when the View gets loaded it tells the Presenter in order for it to go and fetch the data that it requests. That is our first communication with the Presenter.
Now we’re going to do the Table View DataSource. We’re going to to create a new test, called PresenterProvidesNumberOfRowsInSection
. We set XCTAssertEqual(sut)
. This second test is going to verify that the data that the Presenter has will provide the number of rows that the View Controller is going to display.
Let’s go through the three unit test steps again, starting with arrange. We set the mock Presenter that we’re using with the numberOfGeniuses
that we’re going to display, and this is going to be defined by a constant that we will define immediately. And, obviously, we assign the Presenter to the View Controller.
Next, we will change the act part - get the number of rows from the Table View method. And, finally, we’re going to change the assert to be the number of rows that I have obtained from the Table View is exactly the same as I have inserted into the Presenter.
// MARK: - Table View DataSource
func testPresenterProvidesNumberOfRowsInSection() {
presenter.totalGeniuses = numberOfGeniuses
sut.presenter = presenter
let rows = sut.tableView(sut.tableView, numberOfRowsInSection: 0)
XCTAssertEqual(rows, numberOfGeniuses)
}
First, I have to define the constant because otherwise this won’t compile. I have defined the number of geniuses to 7 (you just have to choose something that is not zero, one or default values). Next, I would like to make the Presenter provide the value that I’m expecting. I define the number of geniuses var totalGeniuses: Int?
in order to assign these to the Presenter.
class GeniusesTableViewControllerTests: XCTestCase {
// MARK: - Parameters & Constants.
let storyboardName = "Main"
let numberOfGeniusesOne = 7
// MARK: - Test variables.
var sut: GeniusesTableViewController!
var presenter: GeniusesListPresenterMock!
And, now I have the totalGeniuses and the constant numberOfGeniuses
defined, I can use that in the Presenter mock (sut.presenter = presenter
), obtain that from the Table View Controller (which by default has these methods, I don’t have to override it yet)… and verify that this obviously will fail.
Next, we should go to the View Controller and obtain this data that we have obtained from the mock. We will create the override for the method and return 7. This is the simplest amount of code that I can put in to pass the test (I can go and ask the Presenter, but that would be more complex than returning 7).
override func tableView(tableView: UITableView, numberOfRowsInSection: Int) -> Int {
return 7
}
We will run this and it will crash because when you tell the View Controller that it has some rows it wants to create an instance of those, and since I haven’t implemented the other one, that is something that I have to solve. Let’s go and implement the other method.
I’m just returning the default cell with the reusable ID. And running the test will pass because it’s returning the actual value that I was expecting.
Now let me go back to the test. This has proven that we need a second test in order to prove that functionality. Let’s rename the first constant that we created, the let numberOfGeniuses
and add a second one.
// MARK: - Parameters & Constants.
let storyboardName = "Main"
let numberOfGeniusesOne = 7
let numberOfGeniusesAnother = 3
I go to the “// MARK: - Table View DataSource” and I replace this test (in order to avoid typing numberOfGeniusesOne
many times) with the same version. This is the refactoring of the test. I will add a second one, which has the same content but it uses the numberOfGeniusesAnother
constant. If I run it, it will fail because it is returning the hard coded value. I now have to implement the functionality that I was expected to.
Let’s implement that method in the actual Presenter. I will have the numberOfGeniuses
returned by the Presenter, I don’t implement anything here. I go to the mock and I override the same method, returning the numberOfGeniuses
that is set in the instance variable of the mock.
Finally, I go to the View Controller that I’m creating, and instead of returning 7, I return the actual number that is provided by the Presenter. Notice the coalescing operator and the number five. I didn’t choose zero because zero is the default value and I want to test that part, so I have set it to five.
The coalescing operator should be providing me with a value that makes sense when the Presenter is not defined. And now I’m not testing this part, so I should add a third test for the value returned by that, which sets sut.presenter = nil
, and it will expect a zero as the number of rows for the Presenter. I run that test and it will complain, obviously, because we haven’t implemented that part. We set the default value and run the test again.
Let me show you the last one: the configuration of the cell, and introduce the unit test testPresenterConfiguresCellView
. We’re going to verify that the Table View is able to configure the cell and the Presenter is doing that properly. We go to the arrange part, we set the Presenter, we set at least one genius it can be configured, and then we act to arrange the configuration of one of the cells. We obtain the cell in order to be configured and to make it work.
Let me go to the Presenter. We’re going to have a method that configures the cell. Notice that all of these methods that I have defined in the Presenter are empty; they are only the skeleton that is required in order to write the test for the Table View Controller.
Now I define the same method in the mock. And I will have the same method that we have defined in the Presenter override func configure(cell cell: GeniusTableViewCell, forRow row: Int)
, and we just set the flag to true in order to check that isConfigureCellInvoked
has been called.
Let me change the assert. Now we are setting that the XCTAssertTrue(presenter.isConfigureCellInvoked)
has this flag set to true. Hopefully this will run, but it will fail. To make it run we need to go to the View Controller, go to the creation of the cell, and send that cell to the configure part. That’s it. We run it… and we succeeded!
With these three parts that we have tested we have a Table View Controller running.
Recap (33:39)
- We have gone through the red, green and refactor parts of the test a couple of times
- We have learned how to add the minimal amount of code in order to pass a test
- TDD is not only possible but useful and cool
- Coverage is obviously 100% because we are testing everything before we create it (sometimes you cannot write tests for 100% coverage, but at least 90% is achievable)
- Views can be tested
- Good architecture helps
Find the source code and whole View Controller tested here.
Q & A (35:12)
Q: Do you think it’s important to test the UI with a unit test or a UI test? Because I see that you tested the creating of the View, that for me is mostly UI testing more than unit tests.
Jorge: They aim for two different goals from my perspective. UI testing is better at integration and at noticing that some things appear on the screen as they are expected.
For example, if I don’t have any content in the cells this View is displayed. Or the content is displayed in the proper language. These tests are not checking this part, and while the UI testing will do so. I think they are complementary. Both - if this is going to be your first project with unit testing or even with TDD, I wouldn’t recommend starting with the View Controller, I just wanted to do that to prove that even for the View Controller, which is usually considered to be the toughest part to test, it can be done and it is not that hard.
I know that I went really fast, I know the stuff because I have prepared, but if you see this for the first time I know that it might seem confusing. Check the code, feel free to send me any questions. But, answering your question, I think they are aimed at different objectives. And I wouldn’t disregard one or the other, I would go with both, if I have the time to create the tests.
For example, I love using UI testing to do the capture of the screens. If you have proper dependency injection you can replace that part. Let’s say that you have a social network, something that is really fancy, and you want to avoid to have the tweets or the messages from everybody else without knowing what is going to be displayed in your actual application. You want to have fake content that displays good on the Apple Store. Then, that integration happens and UI testing will help you to do that. So: different objectives.
Q: I saw you were calling the View to check out the number of rows section. How can you be sure that at that point the View exists? because you are calling sut.tableView
but you’re not injecting any view. How can you be sure that the View is created at that point? You initially create the View Controller, then tested the ViewIsCreated
.
Jorge: When you create the View Controller and you create it by doing the instance from the storyboard, the configuration that you have defined in the storyboard goes with it. When you invoke the View everything gets loaded. It’s a side effect that I wouldn’t have programmed myself but the people from UIKit use. When you invoke the first time the View it creates the whole stuff for you, if it isn’t there.
If the View is created, it will always tell you that it is; because even if it is not it will create it when you ask for it. I don’t think I did that test. If I did then I was wrong. I have used sut.view
in order to ensure that the loading happens. I was not testing that the View was created, only that the event viewDidLoad
was happening. The goal of that part is to make it happen in order to fire the event.
Q: Why not just call it at launch?
Jorge: You shouldn’t. The amount of side effects that the UIView class has is enormous and don’t ask me why, but the proper way to do this is to invoke the View. You disregard the return value from the View but you still try to fetch it in order to make the whole process happen. The whole process happen for the test to pass. Otherwise the viewDidLoad
doesn’t invoke this create.
Q: Could you just call viewDidLoad
?
Jorge: No. the recommendation from shouldn’t invoke it directly because the second method is going to ask viewDidLoad
. There would be some side effects of what happened after it called viewDidLoad
. The amount of side effects is unknown, at this stage could be different.
Keep one thing in mind, which I hope this answers your question, the order in which this test will be executed is random, or it should be. you have to be sure that the side effects don’t affect any other test of your system. You better do things that have the minimum amount of side effects as possible. Looks to me like I have the most side effects if you don’t invoke it.
In my opinion it has changed during the years, but I wouldn’t try to fool it because it doesn’t make any sense. Why should I trick it if I have a proper way to do it?
Q: Regarding the mock object you created, is there a library everything that you create within this mock is essentially to, at least so far, it’s a flag to say that something has been called, right?
Jorge: Yes. This was a very simple case, you’re right. it could be much more, it could be richer, yes.
Q: For this case, are you aware of any libraries or perhaps code generators that could do this for you at the moment?
Jorge: There are none that I know of. if you use Objective-C compatibility you can use OCMock, but I wouldn’t recommend to do so, or OCMockito.
Since Swift has no reflection, no proper reflection, not the full stop that you would get with other languages, mocking is very hard to do because of the static nature of the language compared to the dynamic one of the Objective-C. It is really hard to create classes on the fly that capture behavior and provide pre-defined answers to the caller. We are not yet there.
It should happen, in my opinion soon, but as far as I know, there’s none that can do that because of the limitations of the language in terms of reflection.
**Q: Actually Cuckoo does this. It is a code-generator though, it doesn’t use any reflection. It’s very early days, and it all relies on code generation. It’s not wonderful but it does do that if you need it. If you are very much into mocking then it may help. But my experience is that, this kind of making your own mocks really isn’t that hard. Why not just make your own? **
Jorge: I didn’t know of this one. And keep in mind that since you’re defining this class within the context of this test suite, you can have another GeniusesListPresenterMock
in another test suite which could behave completely different from this one and could do anything else, and not related at all to this one.
Q: Can you show the test coverage again? We have all the View tested, but we created the Presenter and we have no test for that. If we’re developing in TDD, we should have started from the bottom to the top, right? We should have started to develop the test of the Presenter?
Jorge: You’re totally right. The thing is, if you take the code that I have shared with you at this URL, it is not limited to this part. The only part that is mock and has no test is the Model because I’m just providing an array of data, but I have tests for the Presenter and for the App Delegate complete, and for the TableViewCell
.
Usually the way you write your applications is you’re going to start by testing the View Controller, because doesn’t make any sense. You start usually by having something in the Model and from there you evolve to the View Controllers. That should be the proper way to do it but since that was the part I wanted to demonstrate.
Just keep something in mind, that’s not unusual that you start creating the skeletons for classes that are not the one that you are testing because of this way of mocking. If you do so, that’s fair, you can create another template for the other one. But I would rather keep on testing one, and then go to the other one. That’s something that may happen even though you do it in the proper order.
Receive news and updates from Realm straight to your inbox