To reduce duplication and rigidity of the programmer test relationship to implementation code, move away from class and methods as the definition of a “unit” in your unit tests. Instead, use the following question to drive your next constraint on the software:

What should the software do next for the user?

The following coding session will provide an example of applying this question. The fictitious application is a micro-blogging tool named “Jitter”. This is a Seattle-based fictitious company that focuses on enabling coffee injected folks write short messages and have common online messaging shorthand to be expanded for easy reading. The user story we are working on is:

So that it is easier to keep up with my kid’s messages, Mothers want to automatically expand their kid’s shorthand

The acceptance criteria for this user story are:

  • LOL, AFAIK, and TTYL are expandable
  • Able to expand lower and upper case versions of shorthand

The existing code already includes a JitterSession class that users obtain when they authenticate into Jitter to see messages from other people they are following. Mothers can follow their children in this application and so will see their messages in the list of new messages. The client application will automatically expand all of the messages written in shorthand.

The following programmer test expects to expand LOL to “laughing out loud” inside the next message in the JitterSession.

public class WhenUsersWantToExpandMessagesThatContainShorthandTest {

    @Test
    public void shouldExpandLOLToLaughingOutLoud() {
        JitterSession session = mock(JitterSession.class);
        when(session.getNextMessage()).thenReturn("Expand LOL please");
        MessageExpander expander = new MessageExpander(session);
        assertThat(expander.getNextMessage(), equalTo("Expand laughing out loud please"));
    }

}

The MessageExpander class did not exist so along the way I created a skeleton of this class to make the code compile. Once the assertion is failing, I then make the test pass with the following implementation code inside the MessageExpander class:

public String getNextMessage() {
    String msg = session.getNextMessage();
    return msg.replaceAll("LOL", "laughing out loud");
}

This is the most basic message expansion I could do for only one instance of shorthand text. I notice that there are different variations of the message that I want to handle. What if LOL is written in lower case? What if it is written as “Lol”? Should it be expanded? Also, what if some variation of LOL is inside a word? It probably should not expand the shorthand in that case except if the characters surrounding it are symbols, not letters. I write all of this down in the programmer test as comments so I don’t forget about all of these.

// shouldExpandLOLIfLowerCase
// shouldNotExpandLOLIfMixedCase
// shouldNotExpandLOLIfInsideWord
// shouldExpandIfSurroundingCharactersAreNotLetters

I then start working through this list of test cases to enhance the message expansion capabilities in Jitter.

@Test
public void shouldExpandLOLIfLowerCase() {
    when(session.getNextMessage()).thenReturn("Expand lol please");
    MessageExpander expander = new MessageExpander(session);
    assertThat(expander.getNextMessage(), equalTo("Expand laughing out loud please"));
}

This forced me to use the java.util.regex.Pattern class to handle case insensitivity.

public String getNextMessage() {
    String msg = session.getNextMessage();
    return Pattern.compile("LOL", Pattern.CASE_INSENSITIVE).matcher(msg).replaceAll("laughing out loud");
}

Now make it so mixed case versions of LOL are not expanded.

@Test
public void shouldNotExpandLOLIfMixedCase() {
    String msg = "Do not expand Lol please";
    when(session.getNextMessage()).thenReturn(msg);
    MessageExpander expander = new MessageExpander(session);
    assertThat(expander.getNextMessage(), equalTo(msg));
}

This forced me to stop using the Pattern.CASE_INSENSITIVE flag in the pattern compilation. Instead I tell it to match only “LOL” or “lol” for replacement.

public String getNextMessage() {
    String msg = session.getNextMessage();
    return Pattern.compile("LOL|lol").matcher(msg).replaceAll("laughing out loud");
}

Next we’ll make sure that if LOL is inside a word it is not expanded.

@Test
public void shouldNotExpandLOLIfInsideWord() {
    String msg = "Do not expand PLOL or LOLP or PLOLP please";
    when(session.getNextMessage()).thenReturn(msg);
    MessageExpander expander = new MessageExpander(session);
    assertThat(expander.getNextMessage(), equalTo(msg));
}

The pattern matching is now modified to use spaces around each variation of valid LOL shorthand.

return Pattern.compile("\\sLOL\\s|\\slol\\s").matcher(msg).replaceAll("laughing out loud");

Finally, it is important that if the characters around LOL are not letters it still expands.

@Test
public void shouldExpandIfSurroundingCharactersAreNotLetters() {
    when(session.getNextMessage()).thenReturn("Expand .lol! please");
    MessageExpander expander = new MessageExpander(session);
    assertThat(expander.getNextMessage(), equalTo("Expand .laughing out loud! please"));
}

The final implementation of the pattern matching code looks as follows.

return Pattern.compile("\\bLOL\\b|\\blol\\b").matcher(msg).replaceAll("laughing out loud");

I will defer refactoring this implementation until I have to expand additional instances of shorthand text. It just so happens that our acceptance criterion for the user story asks that AFAIK and TTYL are expanded, as well. I won’t show the code for the other shorthand variations in the acceptance criteria. However, I do want to discuss how the focus on “what should the software do next” drove the design of this small component.

Driving the software development using TDD focusing on what the software should do next helps guide us to only implement what is needed and with 100% programmer test coverage for all lines of code. For those who have some experience with object-oriented programming will implement the code with high cohesion, modules focused on specific responsibilities, and low coupling, modules that make few assumptions about other module they interact with will do. This is supported by the disciplined application of TDD. The failing programmer test represents something that the software does not do yet. We focus on modifying the software with the simplest implementation that will make the programmer test pass. Then we focus on enhancing the software’s design with the refactoring step. It has been my experience that refactoring refactoring represents most of the effort expended when doing TDD effectively.