Working with greenfield code

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:

Working with greenfield code

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:

Working with greenfield code

Let's look at the following steps:

  1. Create a test class named SurveyResultCompilerTest and add a when_one_opinion_then_result_forecasts_the_opinion()test to compile the overall survey result.

    Note

    We'll follow this convention for the test method names, for example, when_some_condition_then_this_happens. We will use the underscore symbol as a separator.

  2. In this new test method, type in 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:
    Working with greenfield code
  3. 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");
      } 
  4. We need a compiled result after the survey. The result should give us the party name and winning percentage. We can think of 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();
      }
  5. Add the 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;
      }
    }
  6. Verify that when only one person participates in a survey, then the survey result should return a 100 percent winning chance for the political party that the person votes for. The following assertion verifies our assumption:
    @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));
    }
  7. When we run the test, it fails with a 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;
      }
  8. Rerun the test. It fails for an AssertionError. The following is the output:
    Working with greenfield code
  9. We need to modify the code to return 100 percent for 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;
      }
  10. Rerun the test. It will show you a green bar. The following is the output:
    Working with greenfield code
  11. Now we need to add another test to verify that when two persons participate in a poll, and they vote for two different political parties, then the result should portray 50 percent chance for each party. Add a 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));
      }
  12. When we run the test, it fails. It expects 50 percent but gets 100 percent, as shown in the following screenshot:
    Working with greenfield code
  13. We need to modify the code to return 50 percent for 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;
      }
  14. Rerun the test. The second test passes but the first test fails, as shown in the following screenshot:
    Working with greenfield code
  15. We broke the first test. Now we need to revert the changes, but then the second test will fail. We need an algorithm to calculate the percentage. First, we need to store the opinions. Add a 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
    }
  16. Now we need to modify the 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;
    }
  17. Rerun the test. You will get a green bar, as shown in the following screenshot:
    Working with greenfield code
  18. Now add a test for three participants. The following is the test:
      @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));
      }
  19. Look at the test class, and you will find the duplicate code in each test method; clean them. Move the 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"));
      }
    }
  20. The test class looks clean now. Rerun the test to make sure nothing is broken. The following is the test output:
    Working with greenfield code
  21. Revisit the 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;
      }
    }
  22. Rerun the test to make sure nothing is broken. If anything breaks, then immediately revert the changes. The tests should run fine, so we are good to go.
  23. One feature is complete. Now we need to develop a new feature—zone-wise calculation. The existing test cases will safeguard our code. If you break any existing test, immediately revisit your change.

What we just completed is TDD. It has the following benefits:

  • TDD gives us clean, testable, and maintainable code.
  • We document and update the code, but we forget to update the documentation; this creates confusion. You can document your code and keep it updated, or write your code and unit tests in such a way that anybody can understand the intent. In TDD, tests are written to provide enough documentation of code. So, the test is our documentation, but we need to clean the tests too to keep them readable and maintainable.
  • We can write many tests with boundary value conditions, null, zero, negative numbers, and so on, and verify our code. And by passing these boundary values, you're trying to break your own code. No need to package the whole application and ship it to Quality Assurance (QA) or the customer to discover issues.
  • You also avoid over engineering the classes you write. Just write what's needed to make all tests green.
  • Another benefit to incrementally build your code is that your API is easier to work with because the code is written and used at the same time.
..................Content has been hidden....................

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