TDD with MiniTest and EventManager
Let’s go back to EventManager and rebuild it using automated testing.
Getting Started
While TDD has its haters and its believers, we can all agree that it takes a bit of tedius up-front setup. Let’s get it started right:
File Structure
Setup a new project with the following folders and files:
1 2 3 4 5 6 |
|
Where lib
and test
are directories, the others are files.
Install MiniTest
A version of MiniTest is built into Ruby 1.9, but let’s get the latest version from Rubygems along with Rake:
1
|
|
Setup Rake
We’ll use the tool Rake to actually run the tests. Open the Rakefile
in your project directory and add the following:
1 2 3 4 5 |
|
This tells Rake that our tests will be in the directory named test
and the file names will end in _test.rb
.
Save the Rakefile, then try running the tests from your project directory:
1
|
|
Writing a First Test
Open your test/event_manager_test.rb
and start with this:
1 2 3 4 5 6 7 8 9 |
|
Notice that the name of the method is prefixed with test_
. The class can have other methods in it, for example to set things up for your tests, or helper methods. The prefix test_
is how MiniTest identifies a test that needs to be run.
What’s the point of this test? It’s just a quick way to verify that our parts are hooked together properly.
Run the test with rake test
and you’ll find that it fails. The test isn’t able to find an EventManager
class.
Requiring Files
The test needs to load our event_manager.rb
using require
.
Do we require event_manager
, maybe ../event_manager
? Neither will work. You’re running rake
from the root of the project, so any paths you express must be relative to that folder. The event_manager.rb
is in the same root directory, so we need:
1
|
|
Run the rake test
again, and there’s still an error about EventManager
because the file doesn’t actually define the class.
A Stub Class
Open event_manager.rb
and define a simplistic class:
1 2 3 |
|
Save it, run rake test
, and you should finally be passing. Now just do programming!
Building Functionality
Some things are easy to test, some things are hard to test. Most of the time, if testing a feature is difficult it means the feature is poorly concieved.
The user interacts with the program through reading prompts and entering commands. But testing things that use gets
and puts
are difficult.
Tests, especially "unit tests" like we’re writing today, help you understand and design the objects in your system.
Thinking in Objects
What’s the core object of EventManager? It’s not the script runner, it’s not the prompt or command switching, nor the telephone numbers or zipcodes. EventManager is about people. In this problem domain they’re called attendees.
Let’s design an Attendee
object using tests.
attendee_test.rb
Create a file test/attendee_test.rb
like this:
1 2 3 4 5 6 7 8 9 |
|
Run your tests and that’ll fail because it can’t find an Attendee
class.
Using lib
Before you jump to creating a new object in your root directory, let’s talk about lib
. The lib folder is the place where most of your functional code should live. Let’s create lib/attendee.rb
with this starter:
1 2 3 |
|
Then at the top of your attendee_test
:
1
|
|
Run your tests and everything should be passing.
How Should An Attendee Be Created?
We know we’ve got a CSV of attendees. We’ll want to create one Attendee
instance per line of the CSV. CSV rows work like hashes. So let’s model an Attendee
instance being created by a hash of data:
1 2 3 4 5 6 7 |
|
There are a few things to notice. I arbitrarily chose first name, last name, and phone number as the fields I’d like to deal with first. I did not care what the actual headers are in the CSV. In fact, that CSV has awful inconsistencies in the headers.
TDD is about building software the way you want to use it. I want to create attendees with nicely formatted, reasonable hash keys, so I do it. I accept that, somewhere in the future, I’ll need to create a translator object that can convert the crap headers from the CSV into the hash object I’m expecting here. That’s ok.
Also notice that this test has three assertions. In general, that’s not a good idea – a single test should have a single assertion. But I’ll deal with that in the refactor step.
Run the test and it fails with the following:
1 2 3 |
|
"1 for 0" means I’m supplying one and the object is expecting zero. When you call .new
on a class it is actually running two things under the hood: allocating memory and the object’s initializer, if there is one. When we want to affect how an object is created, we override the initialize
method, like this:
1 2 3 4 5 |
|
Run that and…uh-oh. Two failures:
1 2 3 4 5 6 7 8 9 10 11 |
|
The first one looks like the failure we got before, but look closely. It’s now "0 for 1" and the failure is coming from a different test, the "it exists" test. Now I have a question:
Is it the case that (A) the "it exists" test is now outdated and should be deleted entirely, (B) should be updated to pass in a hash (possibly blank), or (C) should we change the Attendee
class to allow calling .new
without a parameter?
This kind of decision makes some developers feel frustrated with TDD. "I just want to write features!" Though it seems small, this question is important. It may significantly affect how Attendee
instances are created in the future. Our TDD process is forcing us to make the decision consciously now, rather that implicitly later.
We decide (C), that it should be valid to create blank attendees with no parameter. Let’s make input
an option parameter in Attendee
:
1 2 3 |
|
Run the tests and we’re down to one failure:
1 2 3 4 |
|
An attendee doesn’t have a first_name
method. We can do the simplest thing that could possibly work and add one:
1 2 3 4 5 6 7 |
|
Run the tests and get this:
1 2 3 4 |
|
I see an easy fix:
1 2 3 |
|
Run the tests again and get the similar failure:
1 2 3 4 |
|
And follow a similar pattern for last_name
and phone_number
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Run the tests and everything passes. Hooray what a sweet class we have!
At this point you have to resist the urge to make a better implementation. Should this class be using attr_accessor
? I think so. But the tests haven’t driven us to that implemention yet.
Let’s write a new one:
1 2 3 4 5 6 7 |
|
Run the test and it fails:
1 2 3 4 |
|
Now, we could go and implement a first_name=
method which sets an instance variable and refactor the first_name
method to return that variable. But we’re not trying to do the most simplistic thing (IE: use as "dumb" of features as possible), we’re trying to do the simplest thing (easy to program). Let’s create an attr_accessor
for the first name and get rid of the previously implemented method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Run the tests and we see a regression. That test which sends in the hash of data and checks that each field is properly set is now failing. We need to actually use the input
hash in the initialize
:
1 2 3 |
|
Run it and, boom, another regression. You can’t call [:first_name]
in the scenario when no parameter was passed in and input
got the default of nil
. We can change the default value to the empty hash {}
instead of nil
to get around the issue:
1 2 3 |
|
Run that and all four tests pass. Write two further tests to drive the addition of last_name
and phone_number
to the attr_accessor
call.
Cleaning Phone Numbers
From inspecting the data (and/or building the project before), we know there are issues with the phone numbers. Many have non-numeric characters like spaces, hyphes, or periods. A handful have an unnecessary leading zero. Others are too short and can’t be repaired. Let’s use tests to guide us through implementing a cleaner.
Focusing on How You Want To Use It
If we’re thinking ahead, we’ve probably realized there will be something like a "clean phone number" method – but resist that thinking. Instead, how do we want to use it. I want the cleaning to be done automatically, like this:
1 2 3 4 |
|
I run the test and see this failure:
1 2 3 4 |
|
Great! Now I can do a simplistic cleaning in the initializer:
1 2 3 4 5 |
|
Run the tests and…that cleanup test passes but others have broken.
Introducing Regressions
When no phone number is passed in, input[:phone_number]
is nil
. Then calling gsub
on nil
blows up. Ugh, let’s add a little guard:
1 2 3 4 5 6 7 8 |
|
Run the tests and we’re passing.
I look at that implementation and know it’s limited. I see we’re not guarding against spaces, commas, parentheses, or other characters that might come up. While it’s impossible to come up with an exhaustive list of everything that could be done wrong, this is totally reasonable:
1 2 3 4 |
|
Run it, see it fail, then go back and add to my implementation:
1 2 3 4 5 6 7 8 |
|
It passes the tests, but fails Ruby decency standards. We can enter the refactoring process: cleaning up code while trying to avoid adding any new significant functionality.
Refactoring Away From .gsub
I think it’d work to use a regular expression and .scan
like this:
1
|
|
Run the tests and I remember that scan returns an array of strings, not a single string, so my tests are failing. Add on a .join
and everything is cool:
1 2 3 |
|
Dealing with Leading Zeros
Some phone numbers in the data are 11 digits and start with a 1. We can safely cut off that one to normalize the number. Starting with a test…
1 2 3 4 |
|
Run it and, of course, it fails. I implement the easiest idea I can think of inside the initialize
method:
1 2 3 4 5 6 |
|
Run the tests and it passes.
Dealing with Too Short or Too Long Numbers
If, after the previous cleanings, a phone number is not 10 digits, I need to throw it away.
1 2 3 4 5 6 7 8 9 |
|
Run them, they fail, then add even more code to this initialize
:
1 2 3 4 5 6 7 8 9 |
|
Run the tests and they pass.
Refactoring Under Test
That’s all the functionality we’re expecting out of number sanitizing, so we can stop writing tests.
But the code…ick. We need to first get it out of the Attendee#initialize
method. Our test harness allows us to do this with confidence:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
The clean_phone_number
method is still pretty ugly, but it’s abstracted out from initialize
which is now looking good. Before we try to simplify the clean_phone_number
method, we have to ask an important question:
What does cleaning a phone number have to do with the idea of an Attendee
?
It doesn’t. This functionality doesn’t belong in the Attendee
class. It belongs in a PhoneNumber
class.
Applying Your Learning
- Create a
phone_number.rb
and accompanyingphone_number_test.rb
- Move or create tests in
phone_number_test.rb
which exercise the class with all these same formatting fixes - Use that class from the
attendee.rb
- Verify that all tests are passing.
- Once you feel good about
PhoneNumber
, you can reduce the number of tests about phone numbers inattendee_test.rb
Next Steps
- Apply this same pattern to dealing with the zipcodes
- Grab the actual CSV header names, and write an adapter/translator that can take in the CSV row objects and create the
Attendee
objects - Write a
Representative
object which, supplied a zipcode, can supply all the information needed for the Congressional Lookup iteration
References
- MiniTest on GitHub: https://github.com/seattlerb/minitest
- MiniTest Quick Reference: http://www.mattsears.com/articles/2011/12/10/minitest-quick-reference
- Most common assertions:
assert
assert_equal
assert_includes
assert_nil
assert_raises