Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
roth_stephan_clean_c20_sustainable_software_development_patt-1.pdf
Скачиваний:
25
Добавлен:
27.03.2023
Размер:
7.26 Mб
Скачать

Chapter 8 Test-Driven Development

}

TEST(ArabicToRomanNumeralsConverterTestCase, 10_isConvertedTo_X) { ASSERT_EQ("X", convertArabicNumberToRomanNumeral(10));

}

TEST(ArabicToRomanNumeralsConverterTestCase, 20_isConvertedTo_XX) { ASSERT_EQ("XX", convertArabicNumberToRomanNumeral(20));

}

TEST(ArabicToRomanNumeralsConverterTestCase, 30_isConvertedTo_XXX) { ASSERT_EQ("XXX", convertArabicNumberToRomanNumeral(30));

}

Remember what I wrote about test code quality in Chapter 2: the quality of the test code must be as high as the quality of the production code. In other words, our tests need to be refactored, because they contain a lot of duplication and should be designed more elegantly. Furthermore, we want to increase their readability and maintainability. But what can we do?

Take a look at the six tests. The verification in the tests is always the same and could be read more generally as: “Check if Arabic number <x> is converted to the Roman numeral <string>.”

A solution could be to provide a dedicated assertion (also known as custom assertion or custom matcher) for that purpose, which can be read in the same way:

checkIf(x).isConvertedToRomanNumeral("string");

More Sophisticated Tests with a Custom Assertion

To implement our custom assertion, we first of all write a unit test that fails, but different from the unit tests we’ve written before:

TEST(ArabicToRomanNumeralsConverterTestCase, 33_isConvertedTo_XXXIII) { checkIf(33).isConvertedToRomanNumeral("XXXII");

}

354

Chapter 8 Test-Driven Development

The probability is very high that the conversion of 33 already works. Therefore, we force the test to fail (RED) by specifying an intentionally wrong result as the expected value (“XXXII”). But this new test also fails due to another reason: the compiler cannot compile the unit test without errors. A function named checkIf() does not exist yet, equally there is no isConvertedToRomanNumeral().

So, we must first satisfy the compiler by writing the custom assertion. This will consist of two parts (Listing 8-15):

•\

A free checkIf(<parameter>) function, returning one instance of a

 

custom assertion class.

•\

The custom assertion class that contains the real assertion method,

 

verifying one or various properties of the tested object.

Listing 8-15.  A Custom Assertion for Roman Numerals

class RomanNumeralAssert { public:

RomanNumeralAssert() = delete;

explicit RomanNumeralAssert(const unsigned int arabicNumber) : arabicNumberToConvert(arabicNumber) { }

void isConvertedToRomanNumeral(std::string_view expectedRomanNumeral) const {

ASSERT_EQ(expectedRomanNumeral, convertArabicNumberToRomanNumeral(arabicNumberToConvert));

}

private:

const unsigned int arabicNumberToConvert;

};

RomanNumeralAssert checkIf(const unsigned int arabicNumber) { RomanNumeralAssert assert { arabicNumber };

return assert;

}

355

Chapter 8 Test-Driven Development

Note  Instead of a free function checkIf(), a static and public class method can also be used in the assertion class. This can be necessary when you're facing ODR violations, for example, clashes of identical function names. Of course, then the namespace name must be prepended when using the class method:

RomanNumeralAssert::checkIf(33).isConvertedToRomanNumeral ("XXXIII");

Now the code can be compiled without errors, but the new test will fail as expected during execution. See Listing 8-16.

Listing 8-16.  An Excerpt from the Output of Google Test on stdout

[ RUN ] ArabicToRomanNumeralsConverterTestCase.33_isConvertedTo_XXXIII

../ArabicToRomanNumeralsConverterTestCase.cpp:30: Failure

Value of: convertArabicNumberToRomanNumeral(arabicNumberToConvert) Actual: "XXXIII"

Expected: expectedRomanNumeral Which is: "XXXII"

[ FAILED ] ArabicToRomanNumeralsConverterTestCase.33_isConvertedTo_XXXIII (0 ms)

So, we need to modify the test and correct the Roman numeral that we expect as the result. See Listing 8-17.

Listing 8-17.  Our Custom Asserter Allows a More Compact Spelling of the Test Code

TEST(ArabicToRomanNumeralsConverterTestCase, 33_isConvertedTo_XXXIII) { checkIf(33).isConvertedToRomanNumeral("XXXIII");

}

Now we can sum up all previous tests into a single one, as shown in Listing 8-18.

Listing 8-18.  All Checks Can Be Elegantly Pooled Into One Test Function

TEST(ArabicToRomanNumeralsConverterTestCase, conversionOfArabicNumbersToRomanNumerals_Works) {

356

Chapter 8 Test-Driven Development

checkIf(1).isConvertedToRomanNumeral("I"); checkIf(2).isConvertedToRomanNumeral("II"); checkIf(3).isConvertedToRomanNumeral("III"); checkIf(10).isConvertedToRomanNumeral("X"); checkIf(20).isConvertedToRomanNumeral("XX"); checkIf(30).isConvertedToRomanNumeral("XXX"); checkIf(33).isConvertedToRomanNumeral("XXXIII");

}

Take a look at our test code now: redundancy-free, clean, and easily readable. The directness of our self-made assertion is quite elegant. And it is blindingly easy to add more tests now, because we have just to write a single line of code for every new test.

You might complain that this refactoring also has a small disadvantage. The name of the test method is now less specific than the name of all test methods prior to the refactoring (see the section on unit test names in Chapter 2). Can we tolerate these small drawbacks? I think yes. We’ve made a compromise here: This little disadvantage is compensated by the benefits in terms of maintainability and extensibility of our tests.

Now we can continue the TDD cycle and implement the production code successively for the following three tests:

checkIf(100).isConvertedToRomanNumeral("C"); checkIf(200).isConvertedToRomanNumeral("CC"); checkIf(300).isConvertedToRomanNumeral("CCC");

After three iterations, the code will look like Listing 8-19 prior to the refactoring step.

Listing 8-19.  Our Conversion Function in the Ninth TDD Cycle Before Refactoring

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;

if (arabicNumber == 100) { romanNumeral = "C";

}else if (arabicNumber == 200) { romanNumeral = "CC";

}else if (arabicNumber == 300) { romanNumeral = "CCC";

}else {

357

Chapter 8 Test-Driven Development

while (arabicNumber >= 10) { romanNumeral += "X"; arabicNumber -= 10;

}

while (arabicNumber >= 1) { romanNumeral += "I"; arabicNumber--;

}

}

return romanNumeral;

}

And again, the same pattern emerges as before with 1, 2, 3; and 10, 20, and 30. We can also use a similar loop for the hundreds, as shown in Listing 8-20.

Listing 8-20.  The Emerging Pattern, as Well as Which Parts of the Code Are Variable and Which Are Identical, Is Clearly Recognizable

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { std::string romanNumeral;

while (arabicNumber >= 100) { romanNumeral += "C"; arabicNumber -= 100;

}

while (arabicNumber >= 10) { romanNumeral += "X"; arabicNumber -= 10;

}

while (arabicNumber >= 1) { romanNumeral += "I"; arabicNumber--;

}

return romanNumeral;

}

358