This section illustrates the three-step rhythm of writing a failing test, coding enough to make it work, and then refactoring it. This is implied greenfield coding as opposed to working with an existing legacy code.
TDD is an evolutionary development approach. It offers test-first development where the production code is written only to satisfy a test, and the code is refactored to improve the code quality. In TDD, unit tests drive the design. You write the code to satisfy a failing test, so it limits the code you write to only what is needed. The tests provide fast automated regression for refactoring and new enhancements.
Kent Beck is the originator of Extreme Programming and TDD. He has authored many books and papers. Visit http://en.wikipedia.org/wiki/Kent_Beck for details.
The following diagram represents the TDD life cycle:
First, we write a failing test, then add code to satisfy the failing test, and then refactor the code and again start with another test.
The following section provides an example of TDD. We'll build a program to conduct an election survey and forecast the result. The program will compile the survey result and display the opinion poll.
The result should present the zone-wise (geographically) poll opinion and overall opinion, such as if there are two zones, east and west, then the result will be presented in the following format:
Let's look at the following steps:
SurveyResultCompilerTest
and add a when_one_opinion_then_result_forecasts_the_opinion()
test to compile the overall survey result.SurveyResultCompiler()
. The compiler will complain that the SurveyResultCompiler
class doesn't exist. Hover the mouse over SurveyResultCompiler
; Eclipse will suggest a quick fix for you. Choose Create class 'SurveyResultCompiler', and create the class in the com.packt.tdd.survey
package under the src
source folder, as shown in the following screenshot:SurveyResultCompiler
is ready. We need to pass an opinion to SurveyResultCompiler
so that it can compile a result. Modify the test to call willVoteFor
and pass an opinion. The compiler will complain that the method doesn't exist. Add the method to SurveyResultCompiler
by following the quick fix options. The following is the test method:@Test public void when_one_opinion_then_result_forecasts_the_opinion() { new SurveyResultCompiler().willVoteFor("Party A"); }
Map
data type. Modify the test again to obtain the result. The following is the modified test:@Test public void when_one_opinion_then_result_forecasts_the_opinion() { SurveyResultCompiler surveyResultCompiler = new SurveyResultCompiler(); surveyResultCompiler.willVoteFor("Party A"); Map<String, BigDecimal> result =surveyResultCompiler.forecastResult(); }
forecastResult
method to the SurveyResultCompiler
class. The following is the SurveyResultCompiler
class:public class SurveyResultCompiler { public void willVoteFor(String opinion) { } public Map<String, BigDecimal> forecastResult() { return null; } }
@Test public void when_one_opinion_then_result_forecasts_the_opinion() { SurveyResultCompiler surveyResultCompiler = new SurveyResultCompiler(); String opinion = "Party A"; surveyResultCompiler.willVoteFor(opinion); Map<String, BigDecimal> result =surveyResultCompiler.forecastResult(); assertEquals(new BigDecimal("100"), result.get(opinion)); }
NullPointerException
. We need to modify the code as follows to return a result:public Map<String, BigDecimal> forecastResult() { Map<String, BigDecimal> result = new HashMap<String, BigDecimal>(); return result; }
AssertionError
. The following is the output:Party A
. The following is the modified code: public Map<String, BigDecimal> forecastResult() {
Map<String, BigDecimal> result = new HashMap<String, BigDecimal>();
result.put("Party A", new BigDecimal("100"));
return result;
}
when_different_opinions_then_forecasts_50_percent_chance_for_each_party
test, and add the following lines to verify the assumption:@Test public void when_different_opinions_then_forecasts_50_percent_chance_for_each_party() { SurveyResultCompiler surveyResultCompiler = new SurveyResultCompiler(); String opinionA = "Party A"; surveyResultCompiler.willVoteFor(opinionA); String opinionB = "Party B"; surveyResultCompiler.willVoteFor(opinionB); Map<String, BigDecimal> result = surveyResultCompiler.forecastResult(); assertEquals(new BigDecimal("50"), result.get(opinionA)); assertEquals(new BigDecimal("50"), result.get(opinionB)); }
Party A
and 50 percent for Party B
. The following is the modified code:public Map<String, BigDecimal> forecastResult() { Map<String, BigDecimal> result = new HashMap<String, BigDecimal>(); result.put("Party A", new BigDecimal("50")); result.put("Party B", new BigDecimal("50")); return result; }
List
to the SurveyResultCompiler
class and store each opinion. The following is the code:public class SurveyResultCompiler { List<String> opinions = new ArrayList<String>(); public void willVoteFor(String opinion) { opinions.add(opinion); } //the result method is ignored for brevity }
forecastResult
method to calculate the percentage. First, loop through the opinions to get the party-wise vote count, such as 10 voters for Party A
and 20 voters for Party B
. Then, we can compute the percentage as vote count * 100 / total votes. The following is the code:public Map<String, BigDecimal> forecastResult() { Map<String, BigDecimal> result = new HashMap<String, BigDecimal>(); Map<String, Integer> countMap = new HashMap<String, Integer>(); for(String party:opinions) { Integer count = countMap.get(party); if(count == null) { count = 1; }else { count++; } countMap.put(party, count); } for(String party:countMap.keySet()) { Integer voteCount = countMap.get(party); int totalVotes = opinions.size(); BigDecimal percentage = new BigDecimal((voteCount*100)/totalVotes); result.put(party, percentage); } return result; }
@Test public void when_three_different_opinions_then_forecasts_33_percent_chance_for_each_party() { SurveyResultCompiler surveyResultCompiler = new SurveyResultCompiler(); String opinionA = "Party A"; surveyResultCompiler.willVoteFor(opinionA); String opinionB = "Party B"; surveyResultCompiler.willVoteFor(opinionB); String opinionC = "Party C"; surveyResultCompiler.willVoteFor(opinionC); Map<String, BigDecimal> result =surveyResultCompiler.forecastResult(); assertEquals(new BigDecimal("33"), result.get(opinionA)); assertEquals(new BigDecimal("33"), result.get(opinionB)); assertEquals(new BigDecimal("33"), result.get(opinionC)); }
SurveyResultCompiler
object instantiation to a setUp
method instead of instantiating the class in each test method. Inline are the opinion
variables, such as opinionA
. The following is the refactored test class:public class SurveyResultCompilerTest { SurveyResultCompiler surveyResultCompiler; @Before public void setUp() { surveyResultCompiler = new SurveyResultCompiler(); } @Test public void when_one_opinion_then_result_forecasts_the_opinion() { surveyResultCompiler.willVoteFor("Party A"); Map<String, BigDecimal> result =surveyResultCompiler.forecastResult(); assertEquals(new BigDecimal("100"), result.get("Party A")); } @Test public void when_two_different_opinions_then_forecasts_50_percent_chance_for_each_party() { surveyResultCompiler.willVoteFor("Party A"); surveyResultCompiler.willVoteFor("Party B"); Map<String, BigDecimal> result =surveyResultCompiler.forecastResult(); assertEquals(new BigDecimal("50"), result.get("Party A")); assertEquals(new BigDecimal("50"), result.get("Party B")); } @Test public void when_three_different_opinions_then_forecasts_33_percent_chance_for_each_party() { surveyResultCompiler.willVoteFor("Party A"); surveyResultCompiler.willVoteFor("Party B"); surveyResultCompiler.willVoteFor("Party C"); Map<String, BigDecimal> result =surveyResultCompiler.forecastResult(); assertEquals(new BigDecimal("33"), result.get("Party A")); assertEquals(new BigDecimal("33"), result.get("Party B")); assertEquals(new BigDecimal("33"), result.get("Party C")); } }
SurveyResultCompiler
class. It works with a List
and two Map
attributes. Do we really need to keep the List
attribute? Instead of calculating the votes from List
, we can directly store the opinions in Map
and keep the opinion count up to date. The following is the refactored code:public class SurveyResultCompiler { private Map<String, Integer> opinions = new HashMap<String, Integer>(); private long participationCount = 0; public void willVoteFor(String opinion) { Integer sameOpinionCount = opinions.get(opinion); if (sameOpinionCount == null) { sameOpinionCount = 1; } else { sameOpinionCount++; } opinions.put(opinion, sameOpinionCount); participationCount++; } public Map<String, BigDecimal> forecastResult() { Map<String, BigDecimal> result = new HashMap<String, BigDecimal>(); for (String opinion : opinions.keySet()) { Integer sameOpinionCount = opinions.get(opinion); BigDecimal opinionPercentage = new BigDecimal((sameOpinionCount * 100) / participationCount); result.put(opinion, opinionPercentage); } return result; } }
What we just completed is TDD. It has the following benefits: