Test-Driven Development - The Bigger Unit-Testing Picture - Pragmatic Unit Testing in Java 8 with JUnit (2015)

Pragmatic Unit Testing in Java 8 with JUnit (2015)

Part 4. The Bigger Unit-Testing Picture

You can take your unit-testing skills to the next level by learning about the practice of test-driven development (TDD). We’ll rewrite some familiar code using TDD so you can experience it firsthand. You’ll then learn how to face some of the tougher challenges in unit testing. Finally, you’ll learn about unit-testing standards, pair programming, continuous integration (CI), and code coverage to understand how unit testing fits into the larger scope of a project team.

Chapter 12. Test-Driven Development

By now you’ve no doubt noted that it’s hard to write unit tests for some code. This difficult legacy code grows partly from a lack of interest in unit testing. In contrast, the more you consider how to unit-test the code you write, the more likely you are to end up with code that’s easier to test. (“Well, duh!” respond Pat and Dale simultaneously.)

Consider always thinking first about how you will test the code you’re about to write. Rather than slap out some code and then figure out how to test it, design a test that describes the code you want to write, then write the code. This reversed approach might seem bizarre or even impossible, but it’s the core element in the unit-testing practice of test-driven development (TDD).

With TDD, you wield unit tests as a tool to help you shape and control your systems. With TDD, unit testing isn’t a pick-and-choose afterthought that often gets shoved to the side; it’s a required part of a disciplined cycle that becomes core to how you build software. Your software will take on a different and perhaps better design if you employ TDD.

In this chapter we’ll recode some of the iloveyouboss application using TDD and talk about some of its nuances as we go.

The Primary Benefit of TDD

With plain ol’ after-the-fact unit testing, the obvious, most significant benefit you gain is increased confidence that the code you write works as expected. With TDD, you gain that same benefit and more!

Systems degrade largely because we don’t strive often or hard enough to keep the code clean. We’re good at quickly adding code into our systems, but on the first pass, it’s more often not-so-great code than good code. We don’t spend a lot of effort cleaning up that initially bad code for many reasons. Pat chimes in with his list:

· “We just have to move on to the next task. We don’t have time to gild the code.”

· “I think the code reads just fine the way it is. I wrote it, I understand it. I can add some comments to the code if you think it’s not clear.”

· “We can refactor the code when we need to make further changes in that area.”

· “It works. Why mess with a good thing? If it ain’t broke, don’t fix it. It’s too easy to break something else when refactoring code.”

Thanks, Pat. With TDD, your fear about changing code can evaporate. Indeed, refactoring is a risky activity, and we’ve all made plenty of mistakes when making seemingly innocuous changes. But if you’re following TDD well, you’re writing unit tests for virtually all cases you implement in the system. Those unit tests give you the freedom you need to continually improve the code.

Starting Simple

TDD is a three-part cycle:

1. Write a test that fails.

2. Get the test to pass.

3. Clean up any code added or changed in the prior two steps.

Your first step is to write a test that defines the behavior you want to build into the system. In general, you seek to write the test that represents the smallest possible—but useful—increment to the code that already exists.

For our exercise, we’re rebuilding the Profile class. We think about the simplest cases that can occur and decide to write a test that demonstrates what happens when the profile is empty (when no answers have been added to it).

(If you have a Profile class, start this exercise by deleting it and any tests for it. Or start fresh in a new package or project.)

We’ll write our tests incrementally. Eclipse lets us know as soon as we’ve coded a problem by underlining the offending code with red squiggly lines. We stop as soon as Eclipse gives us this negative feedback:

iloveyouboss/tdd-1/test/iloveyouboss/ProfileTest.java

package iloveyouboss;

import org.junit.*;

public class ProfileTest {

@Test

public void matchesNothingWhenProfileEmpty() {

new Profile();

}

}

The Profile class doesn’t exist (you deleted it, right?), so Eclipse flags the attempt to create a new Profile. Create the Profile class in the src directory. (In Eclipse, the Quick Fix feature does this dirty work for you. Wonderful!)

iloveyouboss/tdd-2/src/iloveyouboss/Profile.java

package iloveyouboss;

public class Profile {

}

We wrote a tiny piece of a test and then wrote a tiny piece of code, only enough to compile and stop Eclipse from complaining.

Write the rest of the test in the same manner—as soon as Eclipse complains, respond by writing just enough code to compile. You should end up with the following unit test:

iloveyouboss/tdd-3/test/iloveyouboss/ProfileTest.java

package iloveyouboss;

import org.junit.*;

import static org.junit.Assert.*;

public class ProfileTest {

@Test

public void matchesNothingWhenProfileEmpty() {

Profile profile = new Profile();

*

Question question = new BooleanQuestion(1, "Relocation package?");

*

Criterion criterion =

*

new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare);

*

*

boolean result = profile.matches(criterion);

*

*

assertFalse(result);

}

}

We’ve changed the interface to Profile just a bit from how it appeared in Chapter 2, Getting Real with JUnit. The matches() method takes a (single) Criterion rather than a collection of them (a Criteria). Matching on one at a time seems simpler, and we can add the ability to match on a Criterialater.

You always want your tests to fail first, to demonstrate that the desired behavior (that the test describes) doesn’t yet exist in the system.

images/aside-icons/tip.png

When doing TDD, always watch your tests fail first, to avoid costly bad assumptions.

For Profile, that means return true from matches() because the test expects it to return false:

iloveyouboss/tdd-3/src/iloveyouboss/Profile.java

package iloveyouboss;

public class Profile {

*

public boolean matches(Criterion criterion) {

*

return true;

*

}

}

After we demonstrate test failure, we seek the most straightforward way to make the test pass. Flipping the Boolean from true to false does the trick:

iloveyouboss/tdd-4/src/iloveyouboss/Profile.java

package iloveyouboss;

public class Profile {

public boolean matches(Criterion criterion) {

*

return false;

}

}

We take a look at our test and production code. Nothing seems troublesome, so we don’t do any cleanup. We’ve completed one pass of the TDD cycle. So far the hardcoded false return might seem silly to you, but it’s important to following the incremental mentality of TDD. We’ve built one small bit of behavior for the Profile class, and we know it works.

In fact, if you’re using a capable source repository such as Git, now is the time to commit your code. Committing each new bit of behavior as you do TDD makes it easy to back up and change direction as needed.

Adding Another Increment

For each failing test, seek to add only the code needed to pass the test—to add the smallest possible increment. The mentality: build code exactly to the “specifications” that the tests represent. If the tests all pass, you know you could potentially ship the code—the tests document what the system does, no more, no less. You avoid the potential waste of speculative development.

More practically (in terms of following the TDD cycle) writing the smallest amount of code means that in most cases we can write another test that will first fail. Writing more code than needed means you could find yourself writing lots of tests that pass immediately. That might seem like a good thing, but it takes you right back to the old way of slapping out lots of code before getting pertinent feedback. You’d rather know sooner when you code a defect.

The next-simplest case that comes to our minds is that the profile should match when it contains an Answer matching that of the Criterion:

iloveyouboss/tdd-5/test/iloveyouboss/ProfileTest.java

public class ProfileTest {

@Test

public void matchesNothingWhenProfileEmpty() {

Profile profile = new Profile();

Question question = new BooleanQuestion(1, "Relocation package?");

Criterion criterion =

new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare);

boolean result = profile.matches(criterion);

assertFalse(result);

}

*

@Test

*

public void matchesWhenProfileContainsMatchingAnswer() {

*

Profile profile = new Profile();

*

Question question = new BooleanQuestion(1, "Relocation package?");

*

Answer answer = new Answer(question, Bool.TRUE);

*

profile.add(answer);

*

Criterion criterion = new Criterion(answer, Weight.Important);

*

*

boolean result = profile.matches(criterion);

*

*

assertTrue(result);

*

}

}

The changes to make this test pass are small. Implement an add(Answer) method, and have matches() return true as long as the Profile class holds a reference to an Answer object:

iloveyouboss/tdd-5/src/iloveyouboss/Profile.java

package iloveyouboss;

public class Profile {

*

private Answer answer;

public boolean matches(Criterion criterion) {

*

return answer != null;

}

*

public void add(Answer answer) {

*

this.answer = answer;

*

}

}

Cleaning Up Our Tests

After the second pass through the TDD cycle, we have code we can clean up. Not in the Profile class, but in the tests. We want the tests to stay short and clear. Both our tests instantiate Profile. Create a Profile field and move the common initialization to an @Before method:

iloveyouboss/tdd-6/test/iloveyouboss/ProfileTest.java

public class ProfileTest {

*

private Profile profile;

*

@Before

*

public void createProfile() {

*

profile = new Profile();

*

}

*

@Test

*

public void matchesNothingWhenProfileEmpty() {

*

Question question = new BooleanQuestion(1, "Relocation package?");

*

Criterion criterion =

*

new Criterion(new Answer(question, Bool.TRUE), Weight.DontCare);

*

*

boolean result = profile.matches(criterion);

*

*

assertFalse(result);

*

}

*

// ...

}

Rerun the tests to make sure you’ve not broken anything. The beauty of TDD is that you write tests for all features first, which means you should always have the confidence to refactor and clean up what you just wrote. You stave off system entropy this way!

images/aside-icons/tip.png

TDD enables safe refactoring of virtually all of your code.

The tests (we’ve shown only one here) are a little easier to follow without the uninteresting instantiation of Profile in them.

Similarly, extract the creation of the same BooleanQuestion object to an @Before method. When the tests pass again, rename the question field to questionIsThereRelocation to help make the tests more readable:

iloveyouboss/tdd-7/test/iloveyouboss/ProfileTest.java

public class ProfileTest {

private Profile profile;

*

private BooleanQuestion questionIsThereRelocation;

@Before

public void createProfile() {

profile = new Profile();

}

*

@Before

*

public void createQuestion() {

*

questionIsThereRelocation =

*

new BooleanQuestion(1, "Relocation package?");

*

}

*

@Test

*

public void matchesNothingWhenProfileEmpty() {

*

Criterion criterion = new Criterion(

*

new Answer(questionIsThereRelocation, Bool.TRUE), Weight.DontCare);

*

*

boolean result = profile.matches(criterion);

*

*

assertFalse(result);

*

}

*

// ...

}

We can make one more similar refactoring pass to help the tests concisely express what they’re demonstrating. Extract the creation of an Answer object to the @Before method that creates the Question instance. Use the better field name answerThereIsRelocation, and rename the @Beforemethod to better describe what it does:

iloveyouboss/tdd-8/test/iloveyouboss/ProfileTest.java

public class ProfileTest {

private Profile profile;

private BooleanQuestion questionIsThereRelocation;

*

private Answer answerThereIsRelocation;

@Before

public void createProfile() {

profile = new Profile();

}

@Before

*

public void createQuestionAndAnswer() {

questionIsThereRelocation =

new BooleanQuestion(1, "Relocation package?");

*

answerThereIsRelocation =

*

new Answer(questionIsThereRelocation, Bool.TRUE);

}

@Test

public void matchesNothingWhenProfileEmpty() {

*

Criterion criterion =

*

new Criterion(answerThereIsRelocation, Weight.DontCare);

boolean result = profile.matches(criterion);

assertFalse(result);

}

@Test

public void matchesWhenProfileContainsMatchingAnswer() {

*

profile.add(answerThereIsRelocation);

*

Criterion criterion =

*

new Criterion(answerThereIsRelocation, Weight.Important);

boolean result = profile.matches(criterion);

assertTrue(result);

}

}

Many of your refactorings can be easy yet have great impact. Renaming a variable adds tremendous information for the reader. Extracting small pieces of code into helper methods with intention-revealing names—something your IDE makes trivial—similarly goes a long way toward improving your tests.

You’ve built a second piece of behavior. Commit your code and let’s move on.

Another Small Increment

The next test demonstrates that matches returns false when the Profile instance contains no matching Answer object:

iloveyouboss/tdd-9/test/iloveyouboss/ProfileTest.java

public class ProfileTest {

*

private Answer answerThereIsNotRelocation;

// ...

@Before

public void createQuestionAndAnswer() {

questionIsThereRelocation =

new BooleanQuestion(1, "Relocation package?");

answerThereIsRelocation =

new Answer(questionIsThereRelocation, Bool.TRUE);

*

answerThereIsNotRelocation =

*

new Answer(questionIsThereRelocation, Bool.FALSE);

}

// ...

*

@Test

*

public void doesNotMatchWhenNoMatchingAnswer() {

*

profile.add(answerThereIsNotRelocation);

*

Criterion criterion =

*

new Criterion(answerThereIsRelocation, Weight.Important);

*

*

boolean result = profile.matches(criterion);

*

*

assertFalse(result);

*

}

}

To get the test to pass, the matches() method needs to determine if the sole Answer held by the Profile matches the answer stored in the Criterion. We take a quick look at the Answer class to see how to compare answers. We discover that it contains a match() method that takes an Answer as an argument and returns a boolean:

iloveyouboss/tdd-9/src/iloveyouboss/Answer.java

public class Answer {

// ...

public boolean match(Answer otherAnswer) {

// ...

}

// ...

}

(We’re now acting on a need-to-know basis and deliberately hiding the implementation of match(). Trust for now that it does the job.)

We code our solution to take advantage of the match() method, adding a single conditional to matches that passes the test:

iloveyouboss/tdd-9/src/iloveyouboss/Profile.java

package iloveyouboss;

public class Profile {

private Answer answer;

public boolean matches(Criterion criterion) {

*

return answer != null &&

*

answer.match(criterion.getAnswer());

}

// ...

public void add(Answer answer) {

this.answer = answer;

}

}

Commit your code, and remember to do so from here on out.

Part of the thinking part in TDD is determining the next test you need to write. As a programmer, your job requires understanding all the possible permutations and scenarios that the code must handle. To succeed at TDD, you must break those scenarios into tests and tackle them in an order that minimizes the code increment needed to make each test pass.

Supporting Multiple Answers: A Small Design Detour

A profile can contain many answers, so the next test tackles that scenario:

iloveyouboss/tdd-10/test/iloveyouboss/ProfileTest.java

@Test

public void matchesWhenContainsMultipleAnswers() {

profile.add(answerThereIsRelocation);

profile.add(answerDoesNotReimburseTuition);

Criterion criterion =

new Criterion(answerThereIsRelocation, Weight.Important);

boolean result = profile.matches(criterion);

assertTrue(result);

}

Having multiple Answers in the Profile requires a way to store and distinguish them. We choose to store the Answers in a Map where the key is the question text and the value is the associated Answer. (It’d probably be better to use an Answer ID as the key, but Answer has no such thing yet.)

iloveyouboss/tdd-10/src/iloveyouboss/Profile.java

public class Profile {

*

private Map<String,Answer> answers = new HashMap<>();

*

private Answer getMatchingProfileAnswer(Criterion criterion) {

*

return answers.get(criterion.getAnswer().getQuestionText());

*

}

public boolean matches(Criterion criterion) {

*

Answer answer = getMatchingProfileAnswer(criterion);

return answer != null &&

answer.match(criterion.getAnswer());

}

public void add(Answer answer) {

*

answers.put(answer.getQuestionText(), answer);

}

}

As part of the matches() method, we check the return from getMatchingProfileAnswer() to determine whether or not it’s null. This null check seems a little awkward, and we’d like to find a way to get rid of it, or at least hide it elsewhere. We decide to push the check into the “server” code—the match() method that the Answer class implements. Doing so allows us to swap the receiver in the matches() call: rather than code answer.match(criterion.getAnswer()), we can code criterion.getAnswer().match(answer), because criterion.getAnswer() returns a non-null value (at least given the tests we’ve coded).

To facilitate this small refactoring, write a test to demonstrate the new hope for the matches() method in Answer:

iloveyouboss/tdd-10/test/iloveyouboss/AnswerTest.java

public class AnswerTest {

@Test

public void matchAgainstNullAnswerReturnsFalse() {

assertFalse(new Answer(new BooleanQuestion(0, ""), Bool.TRUE)

.match(null));

}

}

The passing implementation in matches() is a simple guard clause: return false if the passed Answer reference is null. Here’s the change to the Answer class:

iloveyouboss/tdd-10/src/iloveyouboss/Answer.java

public boolean match(Answer otherAnswer) {

*

if (otherAnswer == null) return false;

// ...

return question.match(i, otherAnswer.i);

}

Now you can change the matches() method in Profile and eliminate the null check:

iloveyouboss/tdd-11/src/iloveyouboss/Profile.java

public boolean matches(Criterion criterion) {

Answer answer = getMatchingProfileAnswer(criterion);

return criterion.getAnswer().match(answer);

}

Doing TDD doesn’t require you to slavishly drive in all changes to Profile without touching any other code. You bounce over to other classes—Answer in this case—when you need to change their design to serve your needs.

Expanding the Interface

We’re now ready to open up our interface and support passing a Criteria object to matches(). The next test sets the stage for creating that interface:

iloveyouboss/tdd-11/test/iloveyouboss/ProfileTest.java

@Test

public void doesNotMatchWhenNoneOfMultipleCriteriaMatch() {

profile.add(answerDoesNotReimburseTuition);

Criteria criteria = new Criteria();

criteria.add(new Criterion(answerThereIsRelo, Weight.Important));

criteria.add(new Criterion(answerReimbursesTuition, Weight.Important));

boolean result = profile.matches(criteria);

assertFalse(result);

}

A simple hardcoded return gets the test to pass:

iloveyouboss/tdd-11/src/iloveyouboss/Profile.java

public boolean matches(Criteria criteria) {

return false;

}

…and we quickly write the next test. Our refactoring of the tests as we go has paid off by helping keep our TDD cycles short—perhaps a minute or two to put a new test in place. A test that adds multiple Criterion objects to the Criteria and one matching Answer to the profile is a small variation from the prior test:

iloveyouboss/tdd-12/test/iloveyouboss/ProfileTest.java

@Test

public void matchesWhenAnyOfMultipleCriteriaMatch() {

profile.add(answerThereIsRelo);

Criteria criteria = new Criteria();

criteria.add(new Criterion(answerThereIsRelo, Weight.Important));

criteria.add(new Criterion(answerReimbursesTuition, Weight.Important));

boolean result = profile.matches(criteria);

assertTrue(result);

}

The implementation requires a loop to iterate through each Criterion in Criteria:

iloveyouboss/tdd-12/src/iloveyouboss/Profile.java

public boolean matches(Criteria criteria) {

*

for (Criterion criterion: criteria)

*

if (matches(criterion))

*

return true;

return false;

}

To clean the tests a little, we extract the Criteria locals to a field that we initialize in a new @Before method. We also eliminate the temporary result variable that appears in each test. Doing so goes a little against AAA (it combines the act with the assert), but that’s okay—AAA is not a hard-and-fast rule. The result temporary adds no real value, particularly with the repetitive nature of the tests, and they read better without it. Here’s what one of the tests now looks like:

iloveyouboss/tdd-13/test/iloveyouboss/ProfileTest.java

public class ProfileTest {

// ...

*

private Criteria criteria;

*

@Before

*

public void createCriteria() {

*

criteria = new Criteria();

*

}

// ...

@Test

public void matchesWhenAnyOfMultipleCriteriaMatch() {

profile.add(answerThereIsRelo);

criteria.add(new Criterion(answerThereIsRelo, Weight.Important));

criteria.add(new Criterion(answerReimbursesTuition, Weight.Important));

*

assertTrue(profile.matches(criteria));

}

// ...

}

We continue in our test-driven vein, now adding some of the special cases. The next test: if any must-meet criteria are not met, return false:

iloveyouboss/tdd-13/test/iloveyouboss/ProfileTest.java

@Test

public void doesNotMatchWhenAnyMustMeetCriteriaNotMet() {

profile.add(answerThereIsRelo);

profile.add(answerDoesNotReimburseTuition);

criteria.add(new Criterion(answerThereIsRelo, Weight.Important));

criteria.add(new Criterion(answerReimbursesTuition, Weight.MustMatch));

assertFalse(profile.matches(criteria));

}

Getting it to pass is straightforward:

iloveyouboss/tdd-13/src/iloveyouboss/Profile.java

public boolean matches(Criteria criteria) {

*

boolean matches = false;

for (Criterion criterion: criteria) {

if (matches(criterion))

*

matches = true;

*

else if (criterion.getWeight() == Weight.MustMatch)

*

return false;

}

*

return matches;

}

Hmm…the implementation is starting to look a little like the original solution from Chapter 2, Getting Real with JUnit that we ended up refactoring. It’s still cleaner, but note that TDD doesn’t magically produce the best possible design. That’s okay. You have tests, and you can use them to help you refactor toward a better design when you want.

Last Tests

Another special case: matches() returns true when the criterion is marked as “don’t care”:

iloveyouboss/tdd-14/test/iloveyouboss/ProfileTest.java

@Test

public void matchesWhenCriterionIsDontCare() {

profile.add(answerDoesNotReimburseTuition);

Criterion criterion =

new Criterion(answerReimbursesTuition, Weight.DontCare);

assertTrue(profile.matches(criterion));

}

Making the test pass requires adding a new conditional in the matches() method:

iloveyouboss/tdd-14/src/iloveyouboss/Profile.java

public boolean matches(Criterion criterion) {

return

criterion.getWeight() == Weight.DontCare ||

criterion.getAnswer().match(getMatchingProfileAnswer(criterion));

}

The new test passes, but another test breaks—the first one we wrote, matchesNothingWhenProfileEmpty. We could change the test, but note that it demonstrates pretty much the same thing that doesNotMatchWhenNoMatchingAnswer demonstrates. DeletematchesNothingWhenProfileEmpty.

The last need involves calculating the score. This secondary interest in the matches() method is where we recognized that the first implementation (shown in Chapter 2, Getting Real with JUnit) was slightly off, in that it required the matches() method both to return a Boolean value and update the score field—a side effect.

A better design would probably involve the creation of a secondary object that handles the matching. Here’s a stab at a first test in that direction:

iloveyouboss/tdd-15/test/iloveyouboss/ProfileTest.java

@Test

public void scoreIsZeroWhenThereAreNoMatches() {

criteria.add(new Criterion(answerThereIsRelocation, Weight.Important));

ProfileMatch match = profile.match(criteria);

assertThat(match.getScore(), equalTo(0));

}

We leave the exercise of fleshing out the scoring behavior to you. You should end up with the bulk of the matches() logic moved into the new ProfileMatch class, which has its own set of unit tests. The end design is SRP-compliant, leaving Profile as a class that simply holds onto profile data, and ProfileMatch as a class that calculates matches and scores given answers and criteria.

Tests As Documentation

As the final task, let’s revisit the set of test names in ProfileTest:

matchesWhenProfileContainsMatchingAnswer

doesNotMatchWhenNoMatchingAnswer

matchesWhenContainsMultipleAnswers

doesNotMatchWhenNoneOfMultipleCriteriaMatch

matchesWhenAnyOfMultipleCriteriaMatch

doesNotMatchWhenAnyMustMeetCriteriaNotMet

matchesWhenCriterionIsDontCare

scoreIsZeroWhenThereAreNoMatches

We want readers to be able to quickly answer questions about the behavior of the Profile class. The more we craft the tests for it with care, the more the tests can document the behaviors deliberately designed into Profile.

When seeking to better understand a test-driven class, start by reading its test names. The comprehensive set of test names should provide a holistic summary of the intended capabilities of the class. The more the test names are clear and consistent with one another, the better they act as the most trustworthy form of class documentation.

Our test names are not bad, but we can make them better. The tests are part of the ProfileTest class and thus are testing Profile objects, so omit Profile from each of the test names. Also clarify which of the overloaded matches() methods each test pertains to—the one that takes a Criterion or the one that takes a Criteria. Here’s a first pass at a revised set of test names:

matchesCriterionWhenMatchesSoleAnswer

doesNotMatchCriterionWhenNoMatchingAnswerContained

matchesCriterionWhenOneOfMultipleAnswerMatches

doesNotMatchCriteriaWhenNoneOfMultipleCriteriaMatch

matchesCriteriaWhenAnyOfMultipleCriteriaMatch

doesNotMatchWhenAnyMustMeetCriteriaNotMet

alwaysMatchesDontCareCriterion

scoreIsZeroWhenThereAreNoMatches

The test names are a little clearer but seem perhaps a bit verbose. We can go one step further: nothing says we can’t define the tests in more than one test class. Each separate test class, or fixture, can focus on a group of related behaviors. Here’s how we might split ProfileTest:

class Profile_MatchesCriterionTest {

@Test public void trueWhenMatchesSoleAnswer()...

@Test public void falseWhenNoMatchingAnswerContained()...

@Test public void trueWhenOneOfMultipleAnswerMatches()...

@Test public void trueForAnyDontCareCriterion()...

}

class Profile_MatchesCriteriaTest {

@Test public void falseWhenNoneOfMultipleCriteriaMatch()...

@Test public void trueWhenAnyOfMultipleCriteriaMatch()...

@Test public void falseWhenAnyMustMeetCriteriaNotMet()...

}

class Profile_ScoreTest {

@Test public void zeroWhenThereAreNoMatches()...

}

Encoding the behavior we’re testing into the test-class names means we can remove that repetitive information from the individual test names.

images/aside-icons/tip.png

Regularly ensure that your test names work well together.

The Rhythm of TDD

The cycles of TDD are short. Without all the chatter that accompanies this chapter’s example, each cycle of test-code-refactor is perhaps a few minutes. The increments of code written or changed at each step in the cycle are similarly small.

After you establish a rhythm with TDD, it becomes obvious when you’re heading down a rathole. Set a regular time limit of about ten minutes. If you haven’t received any positive feedback (passing tests) in the last ten minutes, discard what you were working on and try again, taking even smaller steps.

Yes, you heard right—throw away bad code. Treat each cycle of TDD as a time-boxed experiment whose test is the hypothesis. If the experiment is going awry, restarting the experiment and shrinking the scope of assumptions (taking smaller steps) can help you pinpoint where things went wrong. The fresh take can often help you derive a better solution in less time than you would have wasted on the mess you were making.

After

In this chapter you got a whirlwind tour of TDD, which takes all the concepts you’ve learned about unit testing and puts them into a simple disciplined cycle: write a test, get it to pass, ensure the code is clean, and repeat. Adopting TDD may change the way you think about design.

When you return to your desk and start applying what you’ve learned about unit testing, you’ll inevitably hit a sticky challenge that makes you ask, “Now how am I gonna test that?” Let’s find out!