Testing Outside the Box

Now we consider a test where the second letter duplicates the first. Hmm...our tests so far always use an uppercase letter followed by lowercase letters, but the algorithm shouldn’t really care. Let’s take a brief pause and implement a couple tests that deal with case considerations. (We could also choose to add to our test list and save the effort for later.)

We don’t have explicit specs for dealing with case, but part of doing TDD well is thinking outside of what we’re given. Creating a robust application requires finding answers to critical concerns that aren’t explicitly specified. (Hint: ask your customer.)

Soundex encodes similar words to the same code to allow for quick and easy comparison. Case matters not in how words sound. To simplify comparing Soundex codes, we need to use the same casing consistently.

c2/32/SoundexTest.cpp
 
TEST_F(SoundexEncoding, UppercasesFirstLetter) {
 
ASSERT_THAT(soundex.encode(​"abcd"​), StartsWith(​"A"​));
 
}

We change the core algorithm outlined in encode to include uppercasing the head of the word, which we expect to be only a single character. (The casting in upperFront avoids potential problems with handling EOF.)

c2/32/Soundex.h
 
std::​string​ encode(​const​ std::​string​& word) ​const​ {
*
return​ zeroPad(upperFront(head(word)) + encodedDigits(tail(word)));
 
}
 
*
std::​string​ upperFront(​const​ std::​string​& ​string​) ​const​ {
*
return​ std::​string​(1,
*
std::toupper(​static_cast​<​unsigned​ ​char​>(​string​.front())));
*
}

Thinking about case considerations prods us to revisit the test IgnoresVowelLikeLetters. Given what we learned earlier, we expect that our code will ignore uppercase vowels just like it ignores lowercase vowels. But we’d like to make sure. We update the test to verify our concern, putting us outside the realm of TDD and into the realm of testing after the fact.

c2/33/SoundexTest.cpp
 
TEST_F(SoundexEncoding, IgnoresVowelLikeLetters) {
*
ASSERT_THAT(soundex.encode(​"BaAeEiIoOuUhHyYcdl"​), Eq(​"B234"​));
 
}

It passes. We could choose to discard our updated test. In this case, we decide to retain the modified test in order to explicitly document the behavior for other developers.

Since we felt compelled to write yet another test that passed immediately, because we weren’t quite sure what would happen, we decide to revisit our code. The code in encodedDigits appears a bit too implicit and difficult to follow. We have to dig a bit to discover the following:

  • Many letters don’t have corresponding encodings.

  • encodedDigit returns the empty string for these letters.

  • Concatenating an empty string to the encodings variable in encodedDigits effectively does nothing.

We refactor to make the code more explicit. First, we change encodedDigit to return a constant named NotADigit when the encodings map contains no entry for a digit. Then we add a conditional expression in encodedDigits to explicitly indicate that NotADigit encodings get ignored. We also alter lastDigit to use the same constant.

c2/34/Soundex.h
*
const​ std::​string​ NotADigit{​"*"​};
 
 
std::​string​ encodedDigits(​const​ std::​string​& word) ​const​ {
 
std::​string​ encoding;
 
for​ (​auto​ letter: word) {
 
if​ (isComplete(encoding)) ​break​;
 
*
auto​ digit = encodedDigit(letter);
*
if​ (digit != NotADigit && digit != lastDigit(encoding))
*
encoding += digit;
 
}
 
return​ encoding;
 
}
 
 
std::​string​ lastDigit(​const​ std::​string​& encoding) ​const​ {
*
if​ (encoding.empty()) ​return​ NotADigit;
 
return​ std::​string​(1, encoding.back());
 
}
 
// ...
 
std::​string​ encodedDigit(​char​ letter) ​const​ {
 
const​ std::unordered_map<​char​, std::​string​> encodings {
 
{​'b'​, ​"1"​}, {​'f'​, ​"1"​}, {​'p'​, ​"1"​}, {​'v'​, ​"1"​},
 
// ...
 
};
 
auto​ it = encodings.find(letter);
*
return​ it == encodings.end() ? NotADigit : it->second;
 
}

(That listing summarizes a few incremental refactoring changes, each verified with passing tests. In other words, we don’t make these changes all at once.)

Let’s move on to a test that deals with the casing of consonants.

c2/35/SoundexTest.cpp
 
TEST_F(SoundexEncoding, IgnoresCaseWhenEncodingConsonants) {
 
ASSERT_THAT(soundex.encode(​"BCDL"​), Eq(soundex.encode(​"Bcdl"​)));
 
}

Our assertion takes on a slightly different form. It declares that the encoding of "BCDL" should be equivalent to the encoding of "Bcdl". In other words, we don’t care what the actual encoding is, as long as the uppercase input resolves to the same encoding as the corresponding lowercase input.

Our solution is to lowercase the letter when querying the encodings lookup table (in encodedDigit).

c2/35/Soundex.h
 
std::​string​ encodedDigit(​char​ letter) ​const​ {
 
const​ std::unordered_map<​char​, std::​string​> encodings {
 
{​'b'​, ​"1"​}, {​'f'​, ​"1"​}, {​'p'​, ​"1"​}, {​'v'​, ​"1"​},
 
// ...
 
};
*
auto​ it = encodings.find(lower(letter));
 
return​ it == encodings.end() ? NotADigit : it->second;
 
}
 
 
private​:
*
char​ lower(​char​ c) ​const​ {
*
return​ std::tolower(​static_cast​<​unsigned​ ​char​>(c));
*
}
..................Content has been hidden....................

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