Getting Started

A common misconception of TDD is that you first define all the tests before building an implementation. Instead, you focus on one test at a time and incrementally consider the next behavior to drive into the system from there.

As a general approach to TDD, you seek to implement the next simplest rule in turn. (For a more specific, formalized approach to TDD, refer to the TPP [The Transformation Priority Premise].) What useful behavior will require the most straightforward, smallest increment of code to implement?

With that in mind, where do we start with test-driving the Soundex solution? Let’s quickly speculate as to what implementing each rule might entail.

Soundex rule #3 appears most involved. Rule #4, indicating when to stop encoding, would probably make more sense once the implementation of other rules actually resulted in something getting encoded. The second rule hints that the first letter should already be in place, so we’ll start with rule #1. It seems straightforward.

The first rule tells us to retain the first letter of the name and...stop! Let’s keep things as small as possible. What if we have only a single letter in the word? Let’s test-drive that scenario.

c2/1/SoundexTest.cpp
Line 1 
#include "gmock/gmock.h"
TEST(SoundexEncoding, RetainsSoleLetterOfOneLetterWord) {
Soundex soundex;
}
  • On line 1, we include gmock, which gives us all the functionality we’ll need to write tests.

  • A simple test declaration requires use of the TEST macro (line 2). The TEST macro takes two parameters: the name of the test case and a descriptive name for the test. A test case, per Google’s documentation, is a related collection of tests that can “share data and subroutines.”[5] (The term is overloaded; to some, a test case represents a single scenario.)

    Reading the test case name and test name together, left to right, reveals a sentence that describes what we want to verify: “Soundex encoding retains [the] sole letter of [a] one-letter word.” As we write additional tests for Soundex encoding behavior, we’ll use SoundexEncoding for the test case name to help group these related tests.

    Don’t discount the importance of crafting good test names—see the following sidebar.

  • On line 3, we create a Soundex object, then...stop! Before we proceed with more testing, we know we’ve just introduced code that won’t compile—we haven’t yet defined a Soundex class! We’ll stop coding our test and fix the problem before moving on. This approach is in keeping with Uncle Bob’s Three Rules of TDD:

    • Write production code only to make a failing test pass.

    • Write no more of a unit test than sufficient to fail. Compilation failures are failures.

    • Write only the production code needed to pass the one failing test.

    (Uncle Bob is Robert C. Martin. See The Three Rules of TDD for more discussion of the rules.

    Seeking incremental feedback can be a great approach in C++, where a few lines of test can generate a mountain of compiler errors. Seeing an error as soon as you write the code that generates it can make it easier to resolve.

    The three rules of TDD aside, you’ll find that sometimes it makes more sense to code the entire test before running it, perhaps to get a better feel for how you should design the interface you’re testing. You might also find that waiting on additional slow compiles isn’t worth the trade-off in more immediate feedback.

    For now, particularly as you are learning TDD, seek feedback as soon as it can be useful. Ultimately, it’s up to you to decide how incrementally you approach designing each test.

The compiler shows that we indeed need a Soundex class. We could add a compilation unit (.h/.cpp combination) for Soundex, but let’s make life easier for ourselves. Instead of mucking with separate files, we’ll simply declare everything in the same file as the test.

Once we’re ready to push our code up or once we experience pain from having everything in one file, we’ll do things the proper way and split the tests from the production code.

c2/2/SoundexTest.cpp
*
class​ Soundex {
*
};
 
 
#include "gmock/gmock.h"
 
 
TEST(SoundexEncoding, RetainsSoleLetterOfOneLetterWord) {
 
Soundex soundex;
 
}
Q.:

Isn’t putting everything into a single file a dangerous shortcut?

A.:

It’s a calculated effort to save time in a manner that incurs no short-term complexity costs. The hypothesis is that the cost of splitting files later is less than the overhead of flipping between files the whole time. As you shape the design of a new behavior using TDD, you’ll likely be changing the interface often. Splitting out a header file too early would only slow you down.

As far as “dangerous” is concerned: are you ever going to forget to split the files before checking in?

Q.:

But aren’t you supposed to be cleaning up code as you go as part of following the TDD cycle? Don’t you want to always make sure that your code retains the highest possible quality?

A.:

In general, yes to both questions. But our code is fine; we’re simply choosing a more effective organization until we know we need something better. We’re deferring complexity, which tends to slow us down, until we truly need it. (Some Agile proponents use the acronym YAGNI—“You ain’t gonna need it.”)

If the notion bothers you deeply, go ahead and separate the files right off the bat. You’ll still be able to follow through with the rest of the exercise. But I’d prefer you first try it this way. TDD provides you with safe opportunities to challenge yourself, so don’t be afraid to experiment with what you might find to be more effective ways to work.

We’re following the third rule for TDD: write only enough production code to pass a test. Obviously, we don’t have a complete test yet. The RetainsSoleLetterOfOneLetterWord test doesn’t really execute any behavior, and it doesn’t verify anything. Still, we can react to each incremental bit of negative feedback (in this case, failing compilation) and respond with just enough code to get past the negative feedback. To compile, we added an empty declaration for the Soundex class.

Building and running the tests at this point gives us positive feedback.

 
[==========] Running 1 test from 1 test case.
 
[----------] Global test environment set-up.
 
[----------] 1 test from SoundexEncoding
 
[ RUN ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord
 
[ OK ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord (0 ms)
 
[----------] 1 test from SoundexEncoding (0 ms total)
 
 
[----------] Global test environment tear-down
 
[==========] 1 test from 1 test case ran. (0 ms total)
 
[ PASSED ] 1 test.

Party time!

Well, not quite. After all, the test does nothing other than construct an instance of the empty Soundex class. Yet we have put some important elements in place. More importantly, we’ve proven that we’ve done so correctly.

Did you get this far? Did you mistype the include filename or forget to end the class declaration with a semicolon? If so, you’ve created your mistake within the fewest lines of code possible. In TDD, you practice safe coding by testing early and often, so you usually have but one reason to fail.

Since our test passes, it might be a good time to make a local commit. Do you have the right tool? A good version control system allows you to commit easily every time the code is green (in other words, when all tests are passing). If you get into trouble later, you can easily revert to a known good state and try again.

Part of the TDD mentality is that every passing test represents a proven piece of behavior that you’ve added to the system. It might not always be something you could ship, of course. But the more you think in such incremental terms and the more frequently you seek to integrate your locally proven behavior, the more successful you’ll be.

Moving along, we add a line of code to the test that shows how we expect client code to interact with Soundex objects.

c2/3/SoundexTest.cpp
 
TEST(SoundexEncoding, RetainsSoleLetterOfOneLetterWord) {
 
Soundex soundex;
 
*
auto​ encoded = soundex.encode(​"A"​);
 
}

We are making decisions as we add tests. Here we decided that the Soundex class exposes a public member function called encode that takes a string argument. Attempting to compile with this change fails, since encode doesn’t exist. The negative feedback triggers us to write just enough code to get everything to compile and run.

c2/4/SoundexTest.cpp
 
class​ Soundex
 
{
*
public​:
*
std::​string​ encode(​const​ std::​string​& word) ​const​ {
*
return​ ​""​;
*
}
 
};

The code compiles, and all the tests pass, which is still not a very interesting event. It’s finally time to verify something useful: given the single letter A, can encode return an appropriate Soundex code? We express this interest using an assertion.

c2/5/SoundexTest.cpp
 
TEST(SoundexEncoding, RetainsSoleLetterOfOneLetterWord) {
 
Soundex soundex;
 
 
auto​ encoded = soundex.encode(​"A"​);
 
*
ASSERT_THAT(encoded, testing::Eq(​"A"​));
 
}

An assertion verifies whether things are as we expect. The assertion here declares that the string returned by encode is the same as the string we passed it. Compilation succeeds, but we now see that our first assertion has failed.

 
[==========] Running 1 test from 1 test case.
 
[----------] Global test environment set-up.
 
[----------] 1 test from SoundexEncoding
 
[ RUN ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord
 
SoundexTest.cpp:21: Failure
 
Value of: encoded
 
Expected: is equal to 0x806defb pointing to "A"
 
Actual: "" (of type std::string)
 
[ FAILED ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord (0 ms)
 
[----------] 1 test from SoundexEncoding (0 ms total)
 
 
[----------] Global test environment tear-down
 
[==========] 1 test from 1 test case ran. (0 ms total)
 
[ PASSED ] 0 tests.
 
[ FAILED ] 1 test, listed below:
 
[ FAILED ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord
 
 
1 FAILED TEST

At first glance, it’s probably hard to spot the relevant output from Google Mock. We first read the very last line. If it says “PASSED,” we stop looking at the test output—all our tests are working! If it says “FAILED” (it does in our example), we note how many test cases failed. If it says something other than “PASSED” or “FAILED,” the test application itself crashed in the middle of a test.

With one or more failed tests, we scan upward to find the individual test that failed. Google Mock prints a [ RUN ] record with each test name when it starts and prints a [ FAILED ] or [ OK ] bookend when the test fails. On failure, the lines between [ RUN ] and [ OK ] might help us understand our failure. In the output for our first failed test shown earlier, we see the following:

 
[ RUN ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord
 
SoundexTest.cpp:21: Failure
 
Value of: encoded
 
Expected: is equal to 0x806defb pointing to "A"
 
Actual: "" (of type std::string)
 
[ FAILED ] SoundexEncoding.RetainsSoleLetterOfOneLetterWord (0 ms)

Paraphrasing this assertion failure, Google Mock expected the local variable encoded to contain the string "A", but the actual string it contained was equal to the empty string.

We expected a failing assertion, since we deliberately hard-coded the empty string to pass compilation. That negative feedback is a good thing and part of the TDD cycle. We want to first ensure that a newly coded assertion—representing functionality we haven’t built yet—doesn’t pass. (Sometimes it does, which is usually not a good thing; see Getting Green on Red.) We also want to make sure we’ve coded a legitimate test; seeing it first fail and then pass when we write the appropriate code helps ensure our test is honest.

The failing test prods us to write code, no more than necessary to pass the assertion.

c2/6/SoundexTest.cpp
 
std::​string​ encode(​const​ std::​string​& word) ​const​ {
*
return​ ​"A"​;
 
}

We compile and rerun the tests. The final two lines of its output indicate that all is well.

 
[==========] 1 test from 1 test case ran. (0 ms total)
 
[ PASSED ] 1 test.

Ship it!

I’m kidding, right? Well, no. We want to work incrementally. Let’s put it this way: if someone told us to build a Soundex class that supported encoding only the letter A, we’d be done. We’d want to clean things up a little bit, but otherwise we’d need no additional logic.

Another way of looking at it is that the tests specify all of the behavior that we have in the system to date. Right now we have one test. Why would we have any more code than what that test states we need?

We’re not done, of course. We have plenty of additional needs and requirements that we’ll incrementally test-drive into the system. We’re not even done with the current test. We must, must, must clean up the small messes we just made.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.223.10