Tip:
Highlight text to annotate it
X
There was a question on Stack Overflow that reflects what I often hear in person. It says,
"I'm really struggling to understand unit testing. I do understand the importance of
TDD, but all the examples of unit testing I read about seem to be extremely simple and
trivial." And he goes on to say, "I think my main issue is I can't find any practical
examples. Here is a method that seems to be a good candidate for testing. What would it
look like to have a useful unit test?" Well, let's take a look, and I'll tell ya!
So here's the method in question. It's called setReminderId. It takes an argument, but it
doesn't actually use it, so I'm going to consider that cruft, and we'll just ignore it. It returns
an NSNumber. And what does it do? Well, the first thing it does is it goes into NSUserDefaults,
and it gets an object with the key "currentReminderId", and it saves that - it presumes that that's
an NSNumber and it saves it away. And then it says, if there is such a value in NSUserDefaults,
then get its integer value, and add 1, and store it back. So, increment it, basically.
Otherwise, if there is no such value stored in the user defaults, then set it to an initial
value of 0. Once it has that, then it goes back to user defaults, and using the same
key, it sets that value back in for that key of "currentReminderId". And then it returns
the value. So basically, it's a persistent counter.
How do you go about unit testing this, and the trick, the stumbling block, is this: This
method is not self-contained, within itself or even within its own class. It has a dependency
on an external object. And in this case, it's NSUserDefaults. We don't want to use the real
NSUserDefaults - well, you could if you really wanted to, but it'd be kind of a pain because
your test would have to sort of save away what was there before (if there was anything),
and then after the test, restore it, so that you wouldn't mess up your manual testing through
the simulator. Instead, what you want to do is inject a fake user defaults. That way,
you can control what's inside of it, what it returns for this top portion, and you can
see what's set on it at the bottom, to make sure that the right thing was done to it.
So… how do we do that? We do that with a thing called Dependency Injection. And there
are a couple ways to do that. One way is to expose it as a property. Then
after creating the object, set the fake user defaults into it. This is called "setter injection."
One thing I like to do with setter injection is have the initializer set up a reasonable
default value for this property - in this case, the real NSUserDefaults singleton.
Another way is to provide it as an extra argument to the initializer. This is called "constructor
injection." A big advantage to constructor injection is that it makes the dependencies
explicit when the object is created. Now, you may worry that you'll end up with a monster
list of arguments. But what this is telling you, what this pain is telling you, is that
you may have too many dependencies — which probably means you have a second class inside
that's trying to get out. So, it's a good thing.
So, which do we use? The rule of thumb is: prefer constructor injection. But you can
always fall back on setter injection if you really need it.
So let's get started with a new Xcode project. Cocoa Touch Static Library is fine (although
there's nothing iOS-specific we're doing today, so it could just as well be for the Mac as
well). But moving along, we'll call it TDDExample. And I'm including unit tests, and we use ARC.
Create… Now, Apple sets us up with a test template
that I really don't like because it has a header and an implementation in separate files:
.h and .m. That's unnecessary for almost all test cases. So let's toss them in the trash.
And since this is a TDD example, I'm going to get rid of the initial template class as
well. We'll just have a clean start. Close these up, since I'm not really going
to use groups. Now, since I don't like Apple's template for
test case classes, what do we do? The answer is, you go to qualitycoding.org and click
on Resources, and then scroll until you find the Unit Test Template. That's mine. Download
it. Now go there in the Terminal, and we'll do a "make install". It shows you what's installed
where. And with that, let's quit Xcode so it has a chance to find that new template.
And there it is: one here for iOS, and another one for OS X.
I am going to add a couple of frameworks to help me out. If you go back to the qualitycoding
Resources page, you'll see I have OCHamcrest. I wrote OCHamcrest wanting to have better
test assertions that were more powerful and less fragile than what OCUnit provides. So
let's download that. And while we're here, for mock objects — now for mock objects,
I do recommend starting off creating them by hand. You'll get a better sense for what
they're for and how they work. But, ah, let's skip ahead and go ahead and download OCMockito,
which I also wrote, for mock objects. And let's get both of those in here. We open
up Hamcrest. You'll see that there are two versions of the framework: one is for OS X
development, the other is for iOS development. We want the iOS one, so we're just going to
drag that in. Make sure it goes into the test target, not the main target. (You can use
Hamcrest for main target stuff, but that's not what we're going to talk about today.)
And that's it - it's done, it's ready! On the Mac, you do have to do a little more setup
of a Copy Files build phase. That's explained in the documentation. So let's grab Mockito;
it's the same story here. Grab the iOS framework and pull it into the test target. And with
that, let's go to Simulator, let's expand this to full-screen. We are ready to roll.
It's time to learn the three steps of "The TDD Waltz." It goes like this. Step one: write
a single failing test. Not a whole bunch of them, just one. Step two: write the simplest
code to pass the test. And all tests should continue to pass. Step three: refactor, to
make the code clean. Both production code and test code should be refactored. And…
repeat! Repeat! And you'll be gliding across the floor.
So back to Xcode, let's find our new test template. There it is! The big green checkmark.
Pull that over into our project. Now I don't know what the class is; I don't have enough
context to give it a name. So I'm going to cop out and call it Example. And so the test
class is ExampleTest. Make sure it goes into the test target. And yes, I'm going to uncomment
these two lines to give me Hamcrest, and these two lines to give me Mockito. Get rid of this
example. And we are ready to roll. Type "test" - now, I have code snippets to help me go
faster. You can get these if you go to my blog, qualitycoding.org, and subscribe, you
get them as a subscriber. Or, of course, you can always make your own. Test. Now this shows
me the basic setup of pretty much every unit test. It's got three parts: given something-something,
when I do something, then what do I expect to happen? Or in other words, this is the
setup of the precondition, this is the execution of the actual part that's being tested, and
this is the verification. So, let's start with with a simple test. What
I want is to see that the method in question returns 0 if NSUserDefaults doesn't have that
key that we want. Remember, if there is no object for that key, it defaults to 0. Using
Hamcrest, I say, "assert that". The object that I'm going to call "sut" for "system under
test". I like doing that across my tests because it lets me quickly copy test code. I'm going
to call the method "nextReminderId" without any extra argument. And I want it to be equal
to 0. Like that. Now, we want the mock object to say - the mock user defaults to say - "No,
I don't have an object for that key." So let's first create mockUserDefaults. And with Mockito,
it's easy: you say, mock class. What class? NSUserDefaults class. And using constructor injection — alloc initWithUserDefaults…
You'll note that none of this has been implemented yet. And I started from the bottom up, because
unless you can express your verification, you haven't yet figured out how to test what
you want to test. So it's good to start from the bottom, and then go back up and say, "How
do I get there?" And we pass in the mock. And we need to set up the mock to say, given
a certain method call that we expect - mockUserDefaults objectForKey of "currentReminderId" — given
that, it will… what? Return nil. Like that. And there's really no separate execution phase,
'cause it's all wrapped up in here. Now you should be able to read this test code, even
if you've never seen Hamcrest or Mockito, and have a pretty decent idea of how it works.
Try to keep your test code dead simple. Let's give it a good name: next reminder ID
with no current reminder ID in user defaults should return zero. It's very long and wordy,
and you notice I'm testing very little. I'm just testing that it returns zero, nothing
else. Now this still doesn't compile, of course. It still doesn't compile, because we need
to create the actual class. Make sure that goes into the main target. And let's get rid
of this file header comment stuff; it's just noise.
OK. So now it's not complaining about Example.h. It is complaining about a couple other things.
Now, let me show you how I set up my screen for the best workflow for TDD. Let's hide
the Utilities panel. I have the test selected. I'm going to option-click on the header file.
That brings it up there. And then, still holding option, I hold shift. So shift-option-click
on the implementation. That brings this up and I… double-click on the plus here to
bring it into the bottom-right. So what this gives me is everything I need: I've got my
test code over here. I've got the class that I'm building over on the right side, with
the header up here and the implementation down here.
Now let's give it these methods that it's complaining about. …That semicolon there,
and you know, we don't really have any initialization yet. I'm just going to kind of ignore that
argument. That's a perfectly valid initializer there; pretty sparse, but it'll do.
Now we need to implement nextReminderId, and it's going to return an NSNumber. Copy and
paste this. What do I want it to return? It has to return something. But I don't want
it to return zero - not yet. I want the test to fail. Remember, step one of test-driven
development is - repeat after me - write a failing test. Unless the test fails, you don't
know that the plumbing is correct. But once you see it fail, in the way you expect it
to fail, then you have confidence in your test.
Let's test! Build succeeded. And… there it is, just what we want. It says, "Expected
0, but was -1". So that proves that the plumbing is correct for it to invoke this method, that
this is the value here we're getting back. So, now the fun. Step two: write the simplest
code that passes the test. Are you ready for this? Are you ready? Here we go… ta-daaa!
Let's run the tests again. …And it finished testing, and we are good.
Now you may complain and say, "This implementation is really stupid." Yes, it is! That's because
it satisfies, in the cleanest way, all the tests we have so far. So you see, as we continually
refactor, we keep the code as clean as possible. In fact, step three, I forgot, is to refactor
the code. There's really nothing to refactor yet, because we're just getting started. So
let's move on, that's the end of Test 1.
Now let's test another requirement: we want this value that was generated to be saved
back into user defaults. It is very, very tempting for the beginner to come here and
add some other verification in here. Do not! No! Resist! Resist the temptation. Ideally,
each unit test has a single verification statement, and we want multiple tests. We're just going
to copy and paste this to get us started, so that we can replace this with something
else. Let me express in the test name what I want it to do: instead of "should return
zero," it's really, "should save zero in user defaults." With all the same setup, only this
time, instead of that, I want to say… I need to ask the user defaults if the thing
I expected was called on it, on the mock object. So I say verify mockUserDefaults setObject
0 was set for the key "currentReminderId". Like that.
Ah. I was like, "Why am I getting an unused variable?" Well that's because I ripped out
the execution portion that was originally in here, in the first test. Really, this is
now going to look like this. And we say, "When I do this, and I don't care about the return
value…" Does this make sense? Given all the same stuff, when I execute that method,
when I invoke that method, verify that setObject:forKey: was called, with these values, on the mock
user defaults. All right, let's test! …By the way, instead of always coming back up
here and clicking this button, the shortcut, the faster way to operate (and fast is always
good in TDD) is command-U. Let's just do that right now, command-U. And I want the test
to fail… and indeed it does. That's good. Step one is complete, because it says, "I
expected this invocation, but I received zero of them." That's exactly what I want.
So now, step two: the simplest code that passes. Well first, we need to modify this to actually
use the user defaults, because we need them in here. Right? What I want is to say, userDefaults
setObj… well… this is just going to get ugly to type. Let me first set up an instance
variable. And say in the initializer, set it to what is passed in. And then finally,
setObject:forKey: …well, what do I want? Well, what's the simplest code that will work?
Again, this looks really stupid. That's OK! …With that, I'm ready to command-U to run
my unit tests again. And it finished testing, and this time we passed. You can check the
log, there's a green checkmark over here to say - to look back and see where things passed
and failed. So this is good, we've done step two. We're not done! Step three is to refactor.
This code looks clean, this code looks clean… ah, but we know, because we copied-and-pasted,
that the test code has duplication. Let's get rid of it.
Let's bring some stuff up into the ivars of the test class. That creates what's called
a "test fixture." So let's go like this, let's copy this to here. That's going to give me
some warnings, so I clean that up like that. And there's another one. Copy that in here.
Make sure that it's using all that. And let's copy these two lines — I'm not going to
copy this one, because this is going to vary from test to test — let's just copy these
two lines. Now, where do they go? Again, using my handy code snippets, I type "setup", then
I get the setup for the test fixture that will be invoked before each test. I can say
like that. And the corresponding teardown. The mock object I don't really care about,
but I want to make sure that the system under test is properly deallocated. That will test
its lifecycle. Now we can get rid of that… get rid of that… Eh, you know what? Do I
really want to keep that down there? I don't know. I don't feel good about leaving this
after all, because I haven't… just because I'm anticipating the future, I need to live
in the present. So let's get rid of that as well. There! Now look at the tests. You see
how this cleans stuff up? But of course, we need to make sure that this worked. Refactoring
has to rely on unit tests. If you're not using unit tests for refactoring, you're not refactoring;
you're doing something else. But this is refactoring. Command-U, run the tests, I expect it to pass…
and indeed it does. So this is a successful refactoring. We are done with Test 2.
Now we want to test that if the user defaults does have a value for the key, then we return
one greater than that value. And as always, step one is to write a failing test. Let's
take this test which checks for zero, and copy and paste it down here, and tweak it.
Next reminder ID with no current reminder ID in user defaults… no, let's change that…
with current reminder ID in user defaults should return - not zero — but one greater.
Let's pick an arbitrary value of 4. In which case, the setup for that will be to return
3, like that. Command-U to run the unit tests, and it fails, and that is good. Step two:
let's write the simplest code to pass this. Come over here, and from user defaults we
get the object for the key in question, and let's put that in a variable. Let's say, if
there is such a value, then return… I'll get its integer value, add one, and return
it back as an NSNumber. Like that, let's try that. Command-U, and we're good. Step three:
refactor. I don't like the way that set up sets up the mock object here with a certain
fake value, but then it's overridden for this test. Let's move this back down — I know,
I just moved it up - but we want to live in the present, not in the future, because the
present will guide us in ways that we might not expect. So now with this, let's run the
unit tests again to see if this refactoring was successful… we're good. Now we have
duplicated code… even this is very similar to this, except for the value. Let's take
this value, nil, and extract. If I were using AppCode, it would be very easy to do with
an Extract Variable refactoring. Xcode is not as kind, we have to do it by hand. So
I'm going to pull this out like that, and I'm pulling it out to set up a refactoring.
I'm making the value… not generic, whatsit… you know what I mean. Programmable. Now I
run the unit tests… expect this to pass… Now I can take this and do a refactor Extract,
and I'm going to call this setUpUserDefaultsWithCurrentReminderId. Like that, that all is fine. No, I don't want
snapshots. So I've extracted this helper method and it's using it here. And now I can inline
this variable. Again, with AppCode, that would be done for me, pretty much. Now I should
be able to take this, bring it down here, and here except the value will be 3. Like
that. Let's see if that works. That's pretty good. Let me add a few comments in here, the
way I like them. Like that. And that gets us a step closer. We're almost there, folks.
Now for our final test, we want to use these same conditions, but instead of checking the
return value, we want to see that it's set back into user defaults with one greater.
Let's take this test as a starting point, copy and paste it, except… next reminder
ID with current reminder ID in user defaults should save not zero, but one greater. And
taking our arbitrary value of 4, and the fake setup for that should be that it returns…
the value in user defaults should be 3, so that it sets 4 back into user defaults. Let's
take this, command-U, expect it to fail… it does fail, and that is good. That is step
one, a failing test. Step two: simplest code to pass. So what do we want to do? We want
to change this… ah, but we're not getting there, this return is getting in the way.
Let's say reminderId equals… and… you know what? Before I implement this, I want
to do some refactoring over here. And in order to do that, don't ever refactor with a failing
test. Always come back to a clean state - slate — clean state - and let's see if we can't
get this to work with our new variable here. Command-U… and look at that, it didn't work!
Why not? Ah, because I forgot that if there is no value, then the default - the initial
value — should be 0. Let's try that. That's better! So now we've refactored. Again, let
me repeat: don't refactor when you have failing tests. Always refactor from green. Now let's
bring this new test back into play, run it to make sure that it fails still …and that's
good. Go like this, and use that variable here, …and we are good. And it's tempting
at this point to say, "Woo-hoo, we're done!" Because it sure looks like the original code
or something close to it, from the problem. The test code looks clean, I don't see anything
really to refactor in here. The header looks clean, this looks clean… ah, but wait. I
just started watching Uncle Bob's Clean Code video series. I highly recommend this series,
you can get it at cleancoders.com. In episode 3, he asks the question, "How many lines should
a function be," or a method, and says, really, the ideal is about four, which is rather remarkable.
Four, huh? This is… let's make this bigger so I can see what's going on here. There's
just our implementation by itself. That's more than four lines. Why? Is it doing more
than one thing? Well… yeah. Yeah, I guess so. Again, Uncle Bob's rule: how do you know
that a function is doing more than one thing? The answer is, if you can extract something
out of it, it's doing more than one thing. Look at this: there's basically two halves
to this. One is determining the next value, and then having determined that value for
the reminder ID, we do some stuff with it. So let's take this, extract it, and the reason
I can do this is because I know my test code is covering me. All this stuff is fully under
test; I can refactor boldly, without fear. Say, determineNextReminderIdFromUserDefaults?
That's pretty good for now. So now it's pulled this out. Let's clean this up a bit, because
now it's setting this variable, and all it's doing is returning it, so really we just can
go back to a straight return here, a straight return here… there, that cleans that up.
Here, of course, we don't need this split across two lines. That cleans that up. Command-U,
I expect this to pass - this is refactoring - to go from green to green — and we're
good. Now I still don't like this. Look at this, we've got this key repeated here, here,
now it's kind of split across two methods. Let's pull that out into a static currentReminderIdKey
…like that. And run the unit tests again, and we pass again. Now, I'm going to use this
constant any place that the key is used here in production code, and I'm not going to use
this, I'm not going to share this with the test code, through the header file. I want
the test code to be ignorant of this, so that, let's say something bad happens and a typo
occurs here. Well this will fail now, because the test has its own — sort-of - knowledge
of what the key should be.
Time for a quick recap. First, avoid Apple's test case template that splits tests into
separate header and implementation. That's simply unnecessary. Instead, use something
like mine, where it's all contained within a single file. You'll just be happier for
it. I like to use OCHamcrest for my test assertions,
and OCMockito for my mock objects. Remember dependency injection, the two types:
there's property injection, but what you want most of all, most often, is constructor injection,
which we used here. I set up my panels so that the entire left
side here is the test code, on the right I have the production code with the header on
the top, and the .m underneath. That way I can see everything I need to. With all the
production code visible right now on the right-hand side, notice how much test code there is.
Watch, look at the scroll bar. See that? It's about twice as much test code as production
code. That's not unusual. Don't be scared of that.
Remember the TDD 3-step waltz. Step one: write a single failing test. Step two: write the
simplest code that passes the test, even if it looks really stupid. Step three: refactor,
and you refactor - everything here is fair game for refactoring - production code and
test code. When refactoring test code, you can pull variables up into ivars. This is
called the test fixture, which is set up and town down before and after each test.
And that's it! Go forth and TDD! Please leave comments, I'd love to hear your experience
with this, especially if you have any questions. If you leave comments on the YouTube page,
I may not see them. Come on over to qualitycoding.org, I've got a link here for the corresponding
blog post for this screencast, and leave your questions there, and I will definitely see
them and respond to them. Thanks a lot!