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

Chapter 8 Test-Driven Development

 

[

----------]

1

test from ArabicToRomanNumeralsConverterTestCase (0 ms total)

[----------

]

Global

test environment

tear-down

[==========]

1

test

from 1 test case

ran. (4 ms total)

[

PASSED ]

1

test.

 

That’s good, because now we know that everything is prepared properly and we can start with our kata.

The First Test

The first step is to decide which first small requirement we want to implement. Then we will write a failing test for it. For our example, we’ve decided to start with converting a single Arabic number into a Roman numeral: We want to convert the Arabic number 1 into an “I.”

Hence, we take the already existing dummy test and convert it into a real unit test, which can prove the fulfillment of this small requirement. Thereby we also have to consider how the interface to the conversion function should look (Listing 8-4).

Listing 8-4.  The First Test (Irrelevant Parts of the Source Code Were Omitted)

TEST(ArabicToRomanNumeralsConverterTestCase, 1_isConvertedTo_I) { ASSERT_EQ("I", convertArabicNumberToRomanNumeral(1));

}

As you can see, we have decided for a simple function that takes an Arabic number as a parameter and has a string as a return value.

But the code cannot be compiled without compiler errors, because the convertArabicNumberToRomanNumeral() function does not yet exist. Uncompilable test code is considered like a failed unit test in TDD.

That means that we now have to stop writing test code to write just enough production code that it can be compiled without errors. Thus, we’re going to create the conversion function now, and we’ll even write that function directly into the source code file, which also contains the test. Of course, we are aware of the fact that it cannot permanently remain this way. See Listing 8-5.

346

Chapter 8 Test-Driven Development

Listing 8-5.  The Function Stub Satisfies the Compiler

#include <gtest/gtest.h>

#include <string>

int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS();

}

std::string convertArabicNumberToRomanNumeral(const unsigned int arabicNumber) {

return "";

}

TEST(ArabicToRomanNumeralsConverterTestCase, 1_isConvertedTo_I) { ASSERT_EQ("I", convertArabicNumberToRomanNumeral(1));

}

Now the code can be compiled again without errors. And for the moment the function returns only an empty string.

In addition, we now have our first executable test, which must fail (RED), because the test expects an “I,” but the function returns an empty string (Listing 8-6).

Listing 8-6.  The Output of Google Test After Executing the Deliberately Failing Unit Test (RED)

[==========]

Running 1 test from 1 test case.

[----------

]

Global test environment set-up.

[----------

]

1 test from ArabicToRomanNumeralsConverterTestCase

[ RUN

]

ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I

../ArabicToRomanNumeralsConverterTestCase.cpp:14: Failure

Value of: convertArabicNumberToRomanNumeral(1)

Actual: ""

 

Expected: "I"

 

[ FAILED

]

ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I (0 ms)

[----------

]

1 test from ArabicToRomanNumeralsConverterTestCase (0 ms total)

[----------

]

Global test environment tear-down

347

Chapter 8 Test-Driven Development

[==========]

1

test from 1 test case ran. (6 ms total)

[

PASSED

]

0

tests.

[

FAILED

]

1

test, listed below:

[

FAILED

]

ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I

1 FAILED TEST

Okay, that’s what we expected.

Now we need to change the implementation of the convertArabicNumberToRomanNumeral() function so that the test will pass. The rule is this: do the simplest thing that could possibly work. And what could be easier than returning an “I” from the function? See Listing 8-7.

Listing 8-7.  The Changed Function (Irrelevant Parts of the Source Code Were Omitted)

std::string convertArabicNumberToRomanNumeral(const unsigned int arabicNumber) {

return "I";

}

You will probably say, “Wait a minute! That’s not an algorithm to convert Arabic numbers into their Roman equivalents. That’s cheating!”

Of course, the algorithm isn’t ready yet. You have to change your mind. The rules of TDD state that we should write the simplest bit of code that passes the current test. It is an incremental process, and we are just at its beginning.

[==========]

Running 1 test from 1 test case.

[----------

]

Global test

environment set-up.

[----------

]

1

test from

ArabicToRomanNumeralsConverterTestCase

[ RUN

]

ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I

[

OK ]

ArabicToRomanNumeralsConverterTestCase.1_isConvertedTo_I (0 ms)

[----------

]

1

test from ArabicToRomanNumeralsConverterTestCase (0 ms total)

[----------

]

Global test

environment tear-down

[==========]

1

test from

1 test case ran. (1 ms total)

[ PASSED ]

1

test.

 

348

Chapter 8 Test-Driven Development

Excellent! The test passed (GREEN) and we can go to the refactoring step. Actually, there is no need to refactor something yet, so we can just proceed with the next runthrough the TDD cycle. But first we have to commit our changes to the source code repository.

The Second Test

For our second unit test, we will take a 2, which has to be converted into “II”.

TEST(ArabicToRomanNumeralsConverterTestCase, 2_isConvertedTo_II) { ASSERT_EQ("II", convertArabicNumberToRomanNumeral(2));

}

Unsurprisingly, this test must immediately fail (RED), because our convertArabicNumberToRomanNumeral() function returns an “I.” After we have verified that the test fails, we complement the implementation so that the test can pass. Once again, we do the simplest thing that could possibly work (Listing 8-8).

Listing 8-8.  We Add Some Code to Pass the New Test

std::string convertArabicNumberToRomanNumeral(const unsigned int arabicNumber) {

if (arabicNumber == 2) { return "II";

}

return "I";

}

Both tests pass (GREEN).

Should we refactor something now? Maybe not yet, but you might get a sneaking suspicion that we will need a refactoring soon. At the moment, we continue with our third test…

The Third Test and the Tidying Afterward

Unsurprisingly, the third test will test the conversion of the number 3:

TEST(ArabicToRomanNumeralsConverterTestCase, 3_isConvertedTo_III) {

349

Chapter 8 Test-Driven Development

ASSERT_EQ("III", convertArabicNumberToRomanNumeral(3));

}

Of course, this test will fail (RED). The code to pass this test, and all previous tests (GREEN), could look as follows:

std::string convertArabicNumberToRomanNumeral(const unsigned int arabicNumber) {

if (arabicNumber == 3) { return "III";

}

if (arabicNumber == 2) { return "II";

}

return "I";

}

The bad gut feeling about the emerging design, which you might have had on the second test, was not unsubstantiated. At least now we, as skilled clean code developers, should be completely dissatisfied with the obvious code duplication. It’s pretty evident that we cannot continue this path. An endless sequence of if statements cannot be a solution, because we will end up with a horrible design. It’s time for refactoring, and we can do it without fear, because 100% unit test coverage creates a comfortable feeling of safety!

If we take a look at the code inside function convertArabicNumberToRomanNumeral(), a pattern is recognizable. The Arabic number is like a counter of the I-characters of its Roman equivalent. In other words, as long as the number to be converted can be decremented by 1 before it reaches 0, an “I” is added to the Roman numeral string.

Well, this can be done in an elegant way using a while loop and string concatenation, as shown in Listing 8-9.

Listing 8-9.  The Conversion Function After Refactoring

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

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

350

Chapter 8 Test-Driven Development

arabicNumber--;

}

return romanNumeral;

}

That looks pretty good. We removed code duplication and found a compact solution. We also had to remove the const declaration from the arabicNumber parameter because we have to manipulate the Arabic number in the function. And the three existing unit tests are still passed.

We can proceed to the next test. Of course, you can also continue with the 5, but I decided for “10-is-X”. I have the hope that the group of 10 will reveal a similar pattern as 1, 2, and 3. The Arabic number 5 will, of course, be treated later. See Listing 8-10.

Listing 8-10.  The Fourth Unit Test

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

}

Well, it shouldn’t surprise anyone that this test fails (RED). Here is what Google Test writes on stdout about this new test:

[ RUN

] ArabicToRomanNumeralsConverterTestCase.10_isConvertedTo_X

../ArabicToRomanNumeralsConverterTestCase.cpp:31: Failure

Value of:

convertArabicNumberToRomanNumeral(10)

Actual:

"IIIIIIIIII"

Expected:

"X"

[ FAILED

] ArabicToRomanNumeralsConverterTestCase.10_isConvertedTo_X (0 ms)

The test fails, because 10 is not “IIIIIIIIII,” but “X”. However, if we see the output of Google Test, we could get an idea. Maybe the same approach that we used for the Arabic numbers 1, 2, and 3, could be used also for 10, 20, and 30?

STOP! Well, that’s imaginable, but we should not yet create something for the future without unit tests that lead us to such a solution. We would not work test-driven anymore, if we implement the production code for 20 and 30 in one go with the code for 10. So, we do again the simplest thing that could possibly work. See Listing 8-11.

351

Chapter 8 Test-Driven Development

Listing 8-11.  The Conversion Function Can Now Also Convert 10

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { if (arabicNumber == 10) {

return "X";

}else {

std::string romanNumeral; while (arabicNumber >= 1) {

romanNumeral += "I"; arabicNumber--;

}

return romanNumeral;

}

}

Okay, the test and all previous tests are passed (GREEN). We can stepwise add a test for the Arabic number 20, and then for 30. After we run through the TDD cycle for both cases, our conversion function looks like Listing 8-12.

Listing 8-12.  The Result During the Sixth TDD Cycle Before Refactoring

std::string convertArabicNumberToRomanNumeral(unsigned int arabicNumber) { if (arabicNumber == 10) {

return "X";

}else if (arabicNumber == 20) { return "XX";

}else if (arabicNumber == 30) { return "XXX";

}else {

std::string romanNumeral; while (arabicNumber >= 1) {

romanNumeral += "I"; arabicNumber--;

}

return romanNumeral;

}

}

352

Chapter 8 Test-Driven Development

At least now a refactoring is urgently required. The emerged code has some bad smells, like some redundancies and a high cyclomatic complexity. However, our suspicion has also been confirmed that the processing of the numbers 10, 20, and 30 follows a similar pattern to processing the numbers 1, 2, and 3. Let’s try it; see Listing 8-13.

Listing 8-13.  After the Refactoring All if-else Decisions Are Gone

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

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

}

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

}

return romanNumeral;

}

Excellent, all tests passed immediately! It seems that we are on the right track. We must, however, have the goal in mind of the refactoring step in the TDD cycle.

Further up in this section, you can read the following: Code duplication and other code smells are eliminated, both from the production code as well as from the unit tests.

We should take a critical look at our test code. Currently it looks like Listing 8-14.

Listing 8-14.  The Emerged Unit Tests Have a Lot of Code Duplications

TEST(ArabicToRomanNumeralsConverterTestCase, 1_isConvertedTo_I) { ASSERT_EQ("I", convertArabicNumberToRomanNumeral(1));

}

TEST(ArabicToRomanNumeralsConverterTestCase, 2_isConvertedTo_II) { ASSERT_EQ("II", convertArabicNumberToRomanNumeral(2));

}

TEST(ArabicToRomanNumeralsConverterTestCase, 3_isConvertedTo_III) {

ASSERT_EQ("III", convertArabicNumberToRomanNumeral(3));

353