Introduction:

Kent Beck, one of the original signers of the Agile manifesto and creator of eXtreme Programming, is credited with discovering Test Driven Development (TDD), but he claims he merely re-popularized the technique and coined the term. Here is what he had to say about rediscovering the technique:

“The original description of TDD was in an ancient book about programming. It said you take the input tape, manually type in the output tape you expect, then program until the actual output tape matches the expected output.
After I’d written the first xUnit framework in Smalltalk I remembered reading this and tried it out. That was the origin of TDD for me. When describing TDD to older programmers, I often hear, “Of course. How else could you program?” Therefore I refer to my role as rediscovering TDD.”

TDD is a highly disciplined approach to developing software. The technique has been around much longer that you may guess. It was not called TDD at the time, but the technique was used way back in the 1950’s by NASA during project Mercury.

 

Tutorial:

Lets use TDD to develop an simple ‘Uasi’ modem class. Uasi is a silly childhood language akin to Pig Latin. Here is how it works:

Exchange every vowel with the next vowel in alphabetical sequence. ‘U’ wraps back around and is replaced by ‘A’.

“My name is Tomas” -> “My nemi os Tumes”

Unit tests exist to prove that the desirable behaviors remain in place throughout future development and refactoring. One test per behavior. How many behaviors/tests do you think an Uasi modem should have? One? A few? Many? Let me rephrase the question: How many regression bugs are possible in an Uasi modem?

I can think of at least 20 desirable behaviors that must be in place for a working Uasi modem, and that’s just to cover the happy paths

  1.  All ‘a’ characters of the encoder input are replaced with ‘e’ characters in the encoded value.
  2.  All ‘e’ characters in the decoder input are replaced with ‘a’ characters in the decoded value.
  3.  All ‘A’ characters of the encoder input are replaced with ‘E’ characters in the encoded value.
  4.  All ‘E’ characters in the decoder input are replaced with ‘A’ characters in the decoded value.

… you get the idea

There are additional expected failure behaviors to consider as well. What is the desired behavior when input is null? Do we expect some kind of InvalidInputException or to return emptyString?

To develop our Uasi modem in a test driven fashion, we will begin the TDD rhythm of “Red, Green, Refactor”.

Red – Write 1 new unit test that covers a single desired behavior. This test should fail because the new desired behavior doesn’t exist yet. This temporary failing test will show up as red in the jUnit report results. That’s how this step gets its name.

Green – Write just enough code to make the test for the new behavior pass. Be mindful not to add any additional behavior yet that is not covered by any tests yet. Your newest test should turn ‘green’ once the new desired behavior is in place.

Refactor – Refactor your testSubject to clean up the implementation of the existing behaviors with confidence that all desired behaviors remain in place because all unit tests continue to pass. Be mindful not to introduce new behaviors that are not covered by tests during refactoring.

Lets start the RGR rhythm with our first test:

public class UasiModemTest {

    UasiModem testSubject;

    @Before public void setUp()
    {
        testSubject = new UasiModem();
    }

    @Test public void testEncodeReplacesLowerCaseACharacterWithLowerCaseECharacter()
    {
        String testInput = "abc";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "ebc";
        assertEquals(expectedResult, testResult);
    }

}

 

At this point the test will fail. Actually it won’t even compile yet. The testSubject class doesn’t exist yet, and neither does its .encode() method.  Lets finish the Red step by making this test compile.

public class UasiModem {

    public String encode(String encodeMe){

        return null;
    }

}

 

Now our first test compiles, but fails when executed. Lets continue to the Green step and make this test pass by implementing the desired behavior with the bare minimum amount of code.

public class UasiModem {

    public String encode(String encodeMe){

        return encodeMe.replace('a', 'e');
    }

}

The unit test of our first behavior now passes, so we can move on the Refactor step where we do cleanup and make the code fit our coding standards.

public class UasiModem {

    public static final char LOWER_A_CHAR = 'a';
    public static final char LOWER_E_CHAR = 'e';

    public String encode(String encodeMe){

        return encodeMe.replace(LOWER_A_CHAR, LOWER_E_CHAR);
    }

}

 

That concludes our first RGR cycle. Continuing the Red, Green, Refactor rhythm will carefully and robustly grow our Uasi modem’s desired behaviors with confidence that regressions are not happening along the way. At the end of our second RGR iteration, our code may look something like this:

public class UasiModemTest {

UasiModem testSubject;

@Before public void setUp()
{
testSubject = new UasiModem();
}

@Test public void testEncodeReplacesLowerCaseACharacterWithLowerCaseECharacter()
{
String testInput = "abc";
String testResult = testSubject.encode(testInput);
String expectedResult = "ebc";
assertEquals(expectedResult, testResult);
}

@Test public void testEncodeReplacesUpperCaseACharacterWithUpperCaseECharacter()
{
String testInput = "ABC";
String testResult = testSubject.encode(testInput);
String expectedResult = "EBC";
assertEquals(expectedResult, testResult);
}

} 

public class UasiModem {

public static final char LOWER_A_CHAR = 'a';
public static final char LOWER_E_CHAR = 'e';
public static final char UPPER_A_CHAR = 'A';
public static final char UPPER_E_CHAR = 'E';

Map<Character, Character> uasiMap = new HashedMap();

public UasiModem(){
uasiMap.put(LOWER_A_CHAR, LOWER_E_CHAR);
uasiMap.put(UPPER_A_CHAR, UPPER_E_CHAR);
}

public String encode(String encodeMe){

StringBuilder returnValueStringBuilder = new StringBuilder();
for(Character character : encodeMe.toCharArray()){
if(uasiMap.containsKey(character)){
returnValueStringBuilder.append(uasiMap.get(character));
}else {
returnValueStringBuilder.append(character);
}
}
return returnValueStringBuilder.toString();
}

}

 

After many RGR iterations, here is the tests I came up with, along with 3 entirely different implementations. I doubt any of these implementations are anything like what you had in mind, but they all work perfectly fine and the tests prove it.

Tests:

public class UasiModemTest {

    private UasiModem testSubject;

    @Before public void setUp()
    {
        testSubject = new UasiModem();
    }

    @Test public void testEncodeReturnsEmptyStringWhenInputIsNull()
    {
        assertEquals("", testSubject.encode(null));
    }

    @Test public void testEncodeReplacesLowerCaseACharacterWithLowerCaseECharacter()
    {
        String testInput = "abc";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "ebc";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesUpperCaseACharacterWithUpperCaseECharacter()
    {
        String testInput = "ABC";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "EBC";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesLowerCaseECharacterWithLowerCaseICharacter()
    {
        String testInput = "def";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "dif";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesUpperCaseECharacterWithUpperCaseICharacter()
    {
        String testInput = "DEF";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "DIF";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesLowerCaseICharacterWithLowerCaseOCharacter()
    {
        String testInput = "hij";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "hoj";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesUpperCaseICharacterWithUpperCaseOCharacter()
    {
        String testInput = "HIJ";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "HOJ";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesLowerCaseOCharacterWithLowerCaseUCharacter()
    {
        String testInput = "nop";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "nup";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesUpperCaseOCharacterWithUpperCaseUCharacter()
    {
        String testInput = "NOP";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "NUP";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesLowerCaseUCharacterWithLowerCaseACharacter()
    {
        String testInput = "tuv";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "tav";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testEncodeReplacesUpperCaseUCharacterWithUpperCaseACharacter()
    {
        String testInput = "TUV";
        String testResult = testSubject.encode(testInput);
        String expectedResult = "TAV";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReturnsEmptyStringWhenInputIsNull()
    {
        assertEquals("", testSubject.decode(null));
    }

    @Test public void testDecodeReplacesLowerCaseACharacterWithLowerCaseUCharacter()
    {
        String testInput = "abc";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "ubc";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesUpperCaseACharacterWithUpperCaseUCharacter()
    {
        String testInput = "ABC";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "UBC";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesLowerCaseECharacterWithLowerCaseACharacter()
    {
        String testInput = "def";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "daf";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesUpperCaseECharacterWithUpperCaseACharacter()
    {
        String testInput = "DEF";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "DAF";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesLowerCaseICharacterWithLowerCaseECharacter()
    {
        String testInput = "hij";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "hej";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesUpperCaseICharacterWithUpperCaseECharacter()
    {
        String testInput = "HIJ";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "HEJ";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesLowerCaseOCharacterWithLowerCaseICharacter()
    {
        String testInput = "nop";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "nip";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesUpperCaseOCharacterWithUpperCaseICharacter()
    {
        String testInput = "NOP";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "NIP";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesLowerCaseUCharacterWithLowerCaseOCharacter()
    {
        String testInput = "tuv";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "tov";
        assertEquals(expectedResult, testResult);
    }

    @Test public void testDecodeReplacesUpperCaseUCharacterWithUpperCaseOCharacter()
    {
        String testInput = "TUV";
        String testResult = testSubject.decode(testInput);
        String expectedResult = "TOV";
        assertEquals(expectedResult, testResult);
    }

Jr Level implementation:

public class UasiModem {

    public String encode(String encodeMe) {
        StringBuilder returnValueStringBuilder = new StringBuilder();
        if(StringUtils.isNotBlank(encodeMe)) {
            for (char character : encodeMe.toCharArray()) {
                switch (character) {
                    case 'A':
                        returnValueStringBuilder.append('E');
                        break;
                    case 'E':
                        returnValueStringBuilder.append('I');
                        break;
                    case 'I':
                        returnValueStringBuilder.append('O');
                        break;
                    case 'O':
                        returnValueStringBuilder.append('U');
                        break;
                    case 'U':
                        returnValueStringBuilder.append('A');
                        break;
                    case 'a':
                        returnValueStringBuilder.append('e');
                        break;
                    case 'e':
                        returnValueStringBuilder.append('i');
                        break;
                    case 'i':
                        returnValueStringBuilder.append('o');
                        break;
                    case 'o':
                        returnValueStringBuilder.append('u');
                        break;
                    case 'u':
                        returnValueStringBuilder.append('a');
                        break;
                    default:
                        returnValueStringBuilder.append(character);
                }

            }
        }
        return returnValueStringBuilder.toString();

    }

    public String decode(String decodeMe) {
        StringBuilder returnValueStringBuilder = new StringBuilder();
        if(StringUtils.isNotBlank(decodeMe)){
            for (char character : decodeMe.toCharArray()) {
                switch (character) {
                    case 'A':
                        returnValueStringBuilder.append('U');
                        break;
                    case 'E':
                        returnValueStringBuilder.append('A');
                        break;
                    case 'I':
                        returnValueStringBuilder.append('E');
                        break;
                    case 'O':
                        returnValueStringBuilder.append('I');
                        break;
                    case 'U':
                        returnValueStringBuilder.append('O');
                        break;
                    case 'a':
                        returnValueStringBuilder.append('u');
                        break;
                    case 'e':
                        returnValueStringBuilder.append('a');
                        break;
                    case 'i':
                        returnValueStringBuilder.append('e');
                        break;
                    case 'o':
                        returnValueStringBuilder.append('i');
                        break;
                    case 'u':
                        returnValueStringBuilder.append('o');
                        break;
                    default:
                        returnValueStringBuilder.append(character);
                }
            }
        }
        return returnValueStringBuilder.toString();
    }
}

Sr. Lever Implementation:

public class UasiModem {

    public static final char LOWER_A_CHAR = 'a';
    public static final char LOWER_E_CHAR = 'e';
    public static final char LOWER_I_CHAR = 'i';
    public static final char LOWER_O_CHAR = 'o';
    public static final char LOWER_U_CHAR = 'u';

    public static final char UPPER_A_CHAR = 'A';
    public static final char UPPER_E_CHAR = 'E';
    public static final char UPPER_I_CHAR = 'I';
    public static final char UPPER_O_CHAR = 'O';
    public static final char UPPER_U_CHAR = 'U';

    private static final Map<Character, Character> ENCODE_MAP = encodeMap();

    private static final Map<Character, Character> DECODE_MAP = decodeMap();

    private static Map<Character, Character> encodeMap() {
        Map<Character, Character> encodeMap = new HashedMap();
        encodeMap.put(LOWER_A_CHAR, LOWER_E_CHAR);
        encodeMap.put(UPPER_A_CHAR, UPPER_E_CHAR);
        encodeMap.put(LOWER_E_CHAR, LOWER_I_CHAR);
        encodeMap.put(UPPER_E_CHAR, UPPER_I_CHAR);
        encodeMap.put(LOWER_I_CHAR, LOWER_O_CHAR);
        encodeMap.put(UPPER_I_CHAR, UPPER_O_CHAR);
        encodeMap.put(LOWER_O_CHAR, LOWER_U_CHAR);
        encodeMap.put(UPPER_O_CHAR, UPPER_U_CHAR);
        encodeMap.put(LOWER_U_CHAR, LOWER_A_CHAR);
        encodeMap.put(UPPER_U_CHAR, UPPER_A_CHAR);
        return Collections.unmodifiableMap(encodeMap);
    }

    private static Map<Character, Character> decodeMap() {
        return Collections.unmodifiableMap(encodeMap().entrySet().stream().collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey)));
    }

    public String encode(String encodeMe)
    {
        return applyMap(ENCODE_MAP, encodeMe);
    }

    public String decode(String decodeMe)
    {
        return applyMap(DECODE_MAP, decodeMe);
    }

    private String applyMap(Map<Character, Character> map, String encodeMe) {
        StringBuilder returnValueStringBuilder = new StringBuilder();
        if(StringUtils.isNotBlank(encodeMe))
        {
            for(Character character : encodeMe.toCharArray())
            {
                returnValueStringBuilder.append(map.containsValue(character) ? map.get(character) : character);
            }
        }
        return returnValueStringBuilder.toString();
    }

}

Master Craftsman Implementation:

Who knows how/why this works, but its fewer lines of code so it must be better 🙂

public class UasiModem {

    public static final String STRING = "AEIOU";

    public String encode(String encodeMe)
    {
        return process(encodeMe, 6);
    }

    public String decode(String decodeMe)
    {
        return process(decodeMe, 4);
    }

    private String process(String input, int offset){
        StringBuilder returnValueStringBuilder = new StringBuilder();
        if(StringUtils.isNotBlank(input))
        {
            for(char character : input.toCharArray())
            {
                if(STRING.indexOf(character)>=0){
                    returnValueStringBuilder.append(STRING.charAt((STRING.indexOf(character)+offset)%STRING.length()));
                }else if(STRING.indexOf(character-32)>=0){
                returnValueStringBuilder.append((char)(STRING.charAt(((STRING.indexOf(character-32)+offset)%STRING.length()))+32));
            }
                else
                    returnValueStringBuilder.append(character);
            }
        }
        return returnValueStringBuilder.toString();
    }

}

 

Now that the tests and a passing implementation are in place, we can refactor with confidence that the desirable behavior is being preserved. Try your own implementation and see if your implementation passes all the tests first try. Get these files from my Github: https://github.com/bkturley/uasiModem.

Try it yourself

As a self directed exercise, try to TDD an “Pig Greek” service. Pig Greek, also known as ‘Obish’, is another silly language sort of like Pig Latin, but easier than Pig Latin to implement because it is encoded by vowel sound position rather than by syllable position. Programmatically breaking English words into syllables is just too complicated and distracts from the purpose of the exercise. Here’s how to speak Pig Greek:

Simply add “Ob” before every vowel sound.

“My horse is in the barn” -> “MOby hOborse Obis Obin thObe bObarn”

This may seems pretty straightforward, but we are dealing with the English language, so there are certainly nuances that make the process non trivial and interesting. The word “my” for example contains no vowels, but does have a vowel sound. The word “horse” has a silent ‘e’ that makes no vowel sound. The word “teeth” has two consecutive vowels, but only one long ‘e’ vowel sound. Correctly decoding Pig Greek has many opportunities for bugs when ‘Ob’ is supposed to be preserved (“microbe”, “obey”, “global”).

 

Fun fact: Special languages like Uasi and Pig Greek that are used for secret communications while among larger groups have a named classification. They are collectively known as ‘Argot’ (pronounced are-go), ‘Cant’, or ‘Cryptolect’.

Leave a Reply

Your email address will not be published.