Slide 1

Slide 1 text

TDD Fundamentals Taking it for a test drive

Slide 2

Slide 2 text

● 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

Slide 3

Slide 3 text

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.

Slide 4

Slide 4 text

What is TDD at its core?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

1. Write a test 2. Make it compile 3. Make it run 4. Remove duplication To quote Kent Beck...

Slide 7

Slide 7 text

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...

Slide 8

Slide 8 text

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...

Slide 9

Slide 9 text

Stats Calculator Example

Slide 10

Slide 10 text

1.Write a test

Slide 11

Slide 11 text

@Test public void testMedian(){ StatsCalculator statsCalculator = new StatsCalculator(); assertThat(statsCalculator.mean(asList(1.5, 5.0, 2.5)), equalTo(3.0)); }

Slide 12

Slide 12 text

2. Make it Compile

Slide 13

Slide 13 text

@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 doubles) { return 0.0; } }

Slide 14

Slide 14 text

3. Make it Pass

Slide 15

Slide 15 text

@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 doubles) { return 3.0; } }

Slide 16

Slide 16 text

BUT THAT’S CHEATING, RIGHT!?

Slide 17

Slide 17 text

4. Remove Duplication

Slide 18

Slide 18 text

@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 doubles) { double sum = 0; for (Double val : doubles) { sum += val; }; return sum/doubles.size(); } }

Slide 19

Slide 19 text

But what about empty lists?

Slide 20

Slide 20 text

1.Write a test

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

2. Make it Compile - No work

Slide 23

Slide 23 text

3. Make it Pass

Slide 24

Slide 24 text

public class StatsCalculatorTest { @Test public void testMedian(){...} @Test public void testMedian_EmptyList(){ StatsCalculator statsCalculator = new StatsCalculator(); try { statsCalculator.mean(new ArrayList()); fail(“should have thrown an exception. Never should get here”); } catch(ArithmeticException e){} } } public class StatsCalculator { public double mean(List 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(); } }

Slide 25

Slide 25 text

Now let’s do Std Deviation

Slide 26

Slide 26 text

Math Refresher

Slide 27

Slide 27 text

1.Write a test

Slide 28

Slide 28 text

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 doubles) {...} }

Slide 29

Slide 29 text

2. Make it Compile

Slide 30

Slide 30 text

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 doubles) {...} public double stdDeviation(List doubles) { return 0.0; } }

Slide 31

Slide 31 text

3. Make it Pass

Slide 32

Slide 32 text

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 doubles) {...} public double stdDeviation(List 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); } }

Slide 33

Slide 33 text

4. Remove Duplication

Slide 34

Slide 34 text

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 doubles) {...} public double stdDeviation(List 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); } }

Slide 35

Slide 35 text

But my apps are complex!

Slide 36

Slide 36 text

DBs, External Services, Batch Processes

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Have No Fear, Top-Down TDD is here

Slide 39

Slide 39 text

Drive out your desired Interfaces via tests

Slide 40

Slide 40 text

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.

Slide 41

Slide 41 text

Example Time!

Slide 42

Slide 42 text

“Can you present a json endpoint to delete a user for our app? I only care if it succeeds or not”

Slide 43

Slide 43 text

Let’s start with an endpoint

Slide 44

Slide 44 text

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 deleteAccount(int userId){ userService.deleteUser(userId); return ResponseEntity(HttpStatus.OK) }

Slide 45

Slide 45 text

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 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)); }

Slide 46

Slide 46 text

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.

Slide 47

Slide 47 text

Now Implement The collaborators with TDD

Slide 48

Slide 48 text

//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”); }}

Slide 49

Slide 49 text

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”); }}

Slide 50

Slide 50 text

And the error case...

Slide 51

Slide 51 text

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”); } }

Slide 52

Slide 52 text

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”); }}

Slide 53

Slide 53 text

Woah! Single Concern Layers!

Slide 54

Slide 54 text

London School (Top-Down, mockist) vs Detroit School (bottom-up, social tests) = false dichotomy

Slide 55

Slide 55 text

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)

Slide 56

Slide 56 text

And we’re done!