Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TDD Fundamentals

TDD Fundamentals

Introduction to the fundamentals of Test Driven Development

David Julia

December 01, 2014
Tweet

More Decks by David Julia

Other Decks in Programming

Transcript

  1. • Exposure to the TDD cycle • Example of how

    to incrementally add features via TDD • Understand how to drive interfaces via mocks • Basic intro of driving code w/tests Goals of This Talk
  2. Nongoals today, but important topics • Discussion of when to

    mock *Advanced topic. Beware over-mocking. • In-depth discussion of top-down vs bottom-up TDD. • Too much discussion about design benefits of listening to your tests. • Deep discussion of refactoring.
  3. Red Green Refactor 2. Write a failing test 3. Write

    the simplest code possible to Make the test pass 4. Refactor: improve the design without changing behavior (keeping all tests passing) 1. Think about what you want to implement next
  4. 1. Write a test 2. Make it compile 3. Make

    it run 4. Remove duplication To quote Kent Beck...
  5. The first three phases need to go by quickly, so

    we get to a known state with the new functionality. You can commit any number of sins to get there, because speed trumps design, just for that brief moment. To quote Kent Beck...
  6. A three legged horse can’t gallop. The first three steps

    of the cycle won’t work without the fourth. Good design at good times. Make it run, make it right. To quote Kent Beck...
  7. @Test public void testMedian(){ StatsCalculator statsCalculator = new StatsCalculator(); assertThat(statsCalculator.mean(asList(1.5,

    5.0, 2.5)), equalTo(3.0)); } public class StatsCalculator { public double mean(List<Double> doubles) { return 0.0; } }
  8. @Test public void testMedian(){ StatsCalculator statsCalculator = new StatsCalculator(); assertThat(statsCalculator.mean(asList(1.5,

    5.0, 2.5)), equalTo(3.0)); } public class StatsCalculator { public double mean(List<Double> doubles) { return 3.0; } }
  9. @Test public void testMedian(){ StatsCalculator statsCalculator = new StatsCalculator(); assertThat(statsCalculator.mean(asList(1.5,

    5.0, 2.5)), equalTo(3.0)); } public class StatsCalculator { public double mean(List<Double> doubles) { double sum = 0; for (Double val : doubles) { sum += val; }; return sum/doubles.size(); } }
  10. public class StatsCalculatorTest { @Test public void testMedian(){...} @Test(expected =

    ArithmeticException.class) public void testMedian_EmptyList(){ StatsCalculator statsCalculator = new StatsCalculator(); statsCalculator.mean(new ArrayList<Double>()); } } public class StatsCalculator { public double mean(List<Double> doubles) { double sum = 0; for (Double val : doubles) { sum += val; }; return sum/doubles.size(); } }
  11. public class StatsCalculatorTest { @Test public void testMedian(){...} @Test public

    void testMedian_EmptyList(){ StatsCalculator statsCalculator = new StatsCalculator(); try { statsCalculator.mean(new ArrayList<Double>()); fail(“should have thrown an exception. Never should get here”); } catch(ArithmeticException e){} } } public class StatsCalculator { public double mean(List<Double> doubles) { if(doubles.isEmpty()){ throw new ArithmeticException("Can't take mean of an empty list"); } double sum = 0; for (Double val : doubles) { sum += val; }; return sum/doubles.size(); } }
  12. public class StatsCalculatorTest { ... @Test public void testStandardDeviation(){ assertThat(statsCalculator.stdDeviation(

    asList(5.0,20.0,40.0,80.0,100.0)), closeTo(40.0625, .1) ); } public class StatsCalculator { public double mean(List<Double> doubles) {...} }
  13. public class StatsCalculatorTest { ... @Test public void testStandardDeviation(){ assertThat(statsCalculator.stdDeviation(

    asList(5.0,20.0,40.0,80.0,100.0)), closeTo(40.0625, .1) ); } public class StatsCalculator { public double mean(List<Double> doubles) {...} public double stdDeviation(List<Double> doubles) { return 0.0; } }
  14. public class StatsCalculatorTest { ... @Test public void testStandardDeviation(){ assertThat(statsCalculator.stdDeviation(

    asList(5.0,20.0,40.0,80.0,100.0)), closeTo(40.0625, .1) ); } public class StatsCalculator { public double mean(List<Double> doubles) {...} public double stdDeviation(List<Double> doubles) { double sum = 0; for (Double val : doubles) { sum += val; }; double avg= sum/doubles.size(); double sumOfDifferencesFromAvgSquared = 0; for (Double val : doubles) { sumOfDifferencesFromAvgSquared += Math.pow(val - avg, 2); }; return Math.sqrt(1.0/(doubles.size()-1) * sumOfDifferencesFromAvgSquared); } }
  15. public class StatsCalculatorTest { ... @Test public void testStandardDeviation(){ assertThat(statsCalculator.stdDeviation(

    asList(5.0,20.0,40.0,80.0,100.0)), closeTo(40.0625, .1) ); } public class StatsCalculator { public double mean(List<Double> doubles) {...} public double stdDeviation(List<Double> doubles) { double mean = mean(doubles); double sumOfDifferencesFromAvgSquared = 0; for (Double val : doubles) { sumOfDifferencesFromAvgSquared += Math.pow(val - mean, 2); }; return Math.sqrt(1.0/(doubles.size()-1) * sumOfDifferencesFromAvgSquared); } }
  16. Process (top down) 1. Locate highest layer @ which to

    make a change (entrypoint). 2. Write a test @ that layer, mocking collaborators that represent significant architectural boundaries. Get them to pass. 3. Write tests for said collaborators, making them pass one at a time.
  17. “Can you present a json endpoint to delete a user

    for our app? I only care if it succeeds or not”
  18. public class AccountsControllerTest { @Test public void deleteAccount() throws Exception

    { when(userService.deleteUser(anyString())).thenReturn(true); this.mockMvc.perform(get("/users/123") andExpect(status().isOk()); verify(userService).deleteUser("123”) }} public interface UserService(){ boolean deleteUser(String userId); } @Controller public class AccountsController(){ @Autowired private UserService userService; @RequestMapping(value ="/users/{userId}", method = RequestMethod.DELETE) public ResponseEntity<String> deleteAccount(int userId){ userService.deleteUser(userId); return ResponseEntity(HttpStatus.OK) }
  19. public interface UserService(){ boolean deleteUser(String userId); } public class AccountsControllerTest

    { @Test public void deleteAccount() throws Exception { when(userService.deleteUser(anyString())).thenReturn(true); this.mockMvc.perform(get("/users/123") andExpect(status().isOk()); verify(userService).deleteUser(123) }} @Controller public class AccountsController(){ @Autowired private UserService userService; //icky @RequestMapping(value ="/users/{userId}", method = RequestMethod.DELETE) public ResponseEntity<String> deleteAccount(int userId){ if(userService.deleteUser(userId)) { return ResponseEntity(HttpStatus.OK); }else { return ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } @Test public void deleteAccount() throws Exception { when(userService.deleteUser(anyString())).thenReturn(false); this.mockMvc.perform(get("/users/123") andExpect(status().is(equalTo(500)); }
  20. Notes • Might return a UserDeletionResult object, not a boolean

    out of the service in real life (usually you don’t just care if something succeeded or failed, but also why). • Would never use field injection ;). Just did it to save space. Unit tests abhor field injection.
  21. //generated stub public class CrazyLegacyUserService implements UserService{ public boolean removeUser(String

    userId){return false;} } public class UserServiceTest { CrazyLegacyUserService userService = new CrazyLegacyUserService(); @Test public void deleteUser() throws Exception { when(legacyUserClient.removeUser(anyString()).thenReturn(I “SUCCESS!!”); userService.deleteUser(“4354ABC”); \ verify(legacyUserClient).removeUser(“4354ABC”); }}
  22. public class CrazyLegacyUserService implements UserService{ public CrazyLegacyUserService( LegacyUserClient client){ this.legacyUserClient

    = legacyUserClient; } public boolean deleteUser(String userId){ legacyUserClient.removeUser(userId); return true; } } public class UserServiceTest { LegacyUserClient mockClient = mock(LegacyUserClient); CrazyLegacyUserService = new CrazyLegacyUserService(legacyUserClient); @Test public void deleteUser() throws Exception { when(legacyUserClient.removeUser(anyString()).thenReturn(“ SUCCESS!!”); boolean result = userService.deleteUser(“4354ABC”); \ assertThat(result, equalTo(true)); verify(legacyUserClient).removeUser(“4354ABC”); }}
  23. Public class CrazyLegacyUserService implements UserService{ //… constructor w/client interface dependency

    omitted for // brevity public boolean deleteUser(String userId){ legacyUserClient.removeUser(userId); return true; }} public class UserServiceTest { //… Test setup omitted for brevity @Test public void deleteUser_handlesErrors() throws Exception { when(legacyUserClient.removeUser(anyString()).thenReturn(“ Oops! Not A success!!”); boolean result = userService.deleteUser(“4354ABC”); \ assertThat(result, equalTo(true)); verify(legacyUserClient).removeUser(“4354ABC”); } }
  24. public class UserService(){ // constructor w/client interface dependency omitted for

    .. // brevity public boolean deleteUser(String userId){ UserDeletionResult result = legacyUserClient.removeUser(userId); return result.isSuccess(); } } public class UserServiceTest { LegacyUserClient legacyUserClient = mock(LegacyUserClient.class); @Test public void deleteUser_handlesErrors() throws Exception { when(legacyUserClient.removeUser(anyString()).thenReturn(“ Oops! Not A success!!”); boolean result = userService.deleteUser(“4354ABC”); \ assertThat(result, equalTo(true)); verify(legacyUserClient).removeUser(“4354ABC”); }}
  25. Separation between client and service in this example seems a

    little contrived, but often its a good pattern to have a “dumb” client that doesn’t interpret semantics of a 3rd party system in terms of your domain (just does structural transformations. On top of that you build a domain-specific service. Notes (aka a wall of text to skip during the presentation)