Is TDD dead or alive?


Published on

Everyone knows about TDD nowadays, but do you feel you are spending more time testing than writing code ? Where is the point where tests become an impediment to the evolution of your project ?

Instead of taking a religious approach to TDD (""you MUST ..."", ""good developers DO ...."", ""have you read the book XYZ on ..."") this is more a professional perspective, looking at how provide value using TDD as a powerful tool to focus on value and reducing waste.

Mockist TDD has lead software to a even higher level of complexity, up to the point where looking at the tests lead to a much higher confusion rather than just reading the code.

We will go through the TDD-related problem and real-life experiences in a very interactive talk, with a common goal to see what we can do now to make our tests more a tool for a better code rather than a karma for our development working life.

Published in: Education, Technology, Business

Is TDD dead or alive?

  1. 1. Twitter: @gitenterprise TDD Dead or Alive? Luca Milanesio GerritForge Platinum Sponsor
  2. 2. 2 .io About me • Luca Milanesio Co-founder of GerritForge • over 20 years of experience in Agile Development SCM and ALM worldwide • Contributor to Jenkins since 2007 (and previously Hudson) • Git SCM mentor for the Enterprise since 2009 • Contributor to Gerrit Code Review community since 2011
  3. 3. 3 .io Agenda  Why?  TDD-induced damage  Evangelist vs. Professional  Reboot  Professional TDD  Learning to write
  4. 4. 4 .io TDD is 20 years old
  5. 5. 5 .io TDD has been widely adopted
  6. 6. 6 .io Hold on … what's that ?
  7. 7. 7 .io … and that ?
  8. 8. 8 .io Test-induced design damage By David Heinemeier Hansson on April 29, 2014 "Code that's hard to test in isolation is poorly designed", goes a common TDD maxim. Isolation meaning free of dependent context and separated from collaborators, especially "slow" ones like database or file IO. The prevalent definition of "unit" in unit testing (though not everyone agrees with this). This is some times true. Some times finding it difficult to test points to a design smell. It may be tangled responsibilities or whatever. But that's a far ways off from declaring that hard-to-unit-test code is always poorly designed, and always in need of repair. That you cannot have well-designed code that is hard to unit test. It's from this unfortunate maxim that much of the test-induced design damage flows. Such damage is defined as changes to your code that either facilitates a) easier test-first, b) speedy tests, or c) unit tests, but does so by harming the clarity of the code through — usually through needless indirection and conceptual overhead. Code that is warped out of shape solely to accommodate testing objectives.
  10. 10. 10 .io Code metrics of one service 340 Java Classes 880 Tests 158 Mocks 99.5% Code Coverage
  11. 11. 11 .io TDD seniority and practices Devs with two digits' years' experience TDD applied with discipline from start Code constantly covered by tests Build time: 2 minutes (~ 130 msec per test)
  12. 12. 12 .io Test example @Test public void shouldGetNewDTOFromEntity() { // Given DTO expectedDTO = DTOFixture.getDefaultRewardWithOneVariant().build(); Reward entity = VariantRewardFixture.getDefaultRewardWithOneVariant().build(); // When DTO variantDTO = entity.transformToDTO(); // Then assertThat(variantDTO, is(not(nullValue()))); assertThat(variantDTO, is(reflectionEquals(expectedDTO))); }
  13. 13. 13 .io Code example public DTO transformToDTO() { String id = getId() == null ? null : getId().toString(); DTO dto = DTO.forVariants(); dto.setId(id); dto.setName(getName()); dto.setDescription(getDescription()); dto.setShortDescription(getShortDescription()); dto.setStatus(getStatus()); dto.setRewardItemType(getRewardItemType()); dto.setDeliveryMechanism(getDeliveryMechanism()); dto.setTermsAndConditions(getTermsAndConditions()); dto.setStockLimited(isStockLimited()); dto.setImages(transformImagesToDTOs(getImages())); for (Variant variant : getVariants()) { dto.addVariant(variant.transformToDTO()); } return dto; }
  14. 14. 14 .io THAT'S GOOD TDD OR NOT?
  15. 15. 15 .io Features velocity For every new feature we did: End-to-End user-journeys acceptance test Service-to-Service integration test Component-to-component integration tests Unit tests (with mocks)
  16. 16. 16 .io … and their planning meeting [TechLead]"How many points for this story ?" [Devs] "2 .. 5 .. 5 .. 5" [TechLead]"5 ? C'mon … for that small change?" [Devs] "Change is trivial, but it will take some time to amend all the tests and getting a green build"
  17. 17. 17 .io But with more quality?  At the beginning yes   Project grew, tests base grew as well  Breaking tests became the norm   Less frequent refactor, for not breaking tests  Tech debt increased over time
  18. 18. 18 .io WHAT WENT WRONG?
  19. 19. 19 .io I AM NOT TDD EVANGELIST Evangelists are "relaying information about a particular set of beliefs with the intention of converting the recipient"
  20. 20. 20 .io I AM TDD PROFESSIONAL Professionals are "members of the profession with the particular knowledge and skills necessary to perform the role of that profession"
  22. 22. 22 .io Step 1 – TEST RED Write the test, without any code written yet RED: test fails (compilation errors are considered failures as well)
  23. 23. 23 .io Step 2 – TEST GREEN Make test work as quickly as possible GREEN: test passes (code satisfies the test assertions)
  24. 24. 24 .io Step 3 – CODE REFACTOR Keep the test code unchanged Rework the implementation to make it clean, DRY, flexible GREEN: new code passes the test (reworked code behaves exactly as before)
  27. 27. 27 .io BUT WHAT IF TEST IS NOT 100% CORRECT?
  29. 29. 29 .io Step 1 – TEST RED-ish Write the [incorrect] test, no code exists yet RED: fails, but maybe the test is wrong? (nothing works, who can tell what is really wrong?)
  30. 30. 30 .io Step 2 – TEST GREEN-ish Make test work as quickly as possible GREEN: [incorrect] test passes (is code correct or wrong? nobody really knows)
  31. 31. 31 .io Step 3 – CODE REFACTOR-ish Keep the test code unchanged Rework the implementation to make it clean, DRY, flexible GREEN: reworked code passes the tests (was the code correct before? Is the code still correct after?)
  32. 32. 32 .io ? What is TESTING? How to TEST the length of a table? Use your foot Or a RULER?
  33. 33. 33 .io How can I test the TEST? How do you test a ruler? ? Your thumb? Or a digital caliper?
  34. 34. 34 .io And how can I test the test of a TEST? How do you test a digital caliper? YOU DON'T [One meter is express in terms of speed of light]
  35. 35. 35 .io Test and accuracy TEST has to be MORE ACCURATE than CODE
  36. 36. 36 .io Test and confidence TEST cannot be tested but we have CONFIDENCE to be VALID K = 299,792,458 m / s
  37. 37. 37 .io Can this test @Test public void shouldRedisplayEditFormWhenErrorAndUserKeyExists() { boolean partnerTokenExists = true; setupForRedisplayEditFormWhenError(partnerTokenExists); ModelAndView mav = partnerController.update(RESOURCE_URI, mockUserPayload, mockBindingResult, mockSessionStatus); verify(mockUserPayload, times(2)).getUserSupplyTypesCodes(); assertThat(mav.getViewName(), equalTo("administration/partners/edit")); assertThat(mockUserPayload.getUserSupplyTypesCodes().size(), equalTo(1)); assertThat(mockUserPayload.getUserSupplyTypesCodes().iterator().next(), equalTo("Identity")); verify(mockSessionStatus, never()).setComplete(); verify(mockCountryService, times(1)).getAllCountries(); verify(mockUserService, times(1)).getAllSupplyTypes(); } private void setupForRedisplayEditFormWhenError(boolean partnerTokenExists) { when(mockBindingResult.hasErrors()).thenReturn(true); when(mockUserPayload.getTokensDefined()).thenReturn(partnerTokenExists); when(mockUserPayload.getUserSupplyTypesCodes()).thenReturn(new HashSet<String>()); when(batchService.getAllByUser(anyString())).thenReturn(null); UserRole partnerRole = new UserRole("PTN1"); partnerRole.setIssuingUser(true); when(mockCurrencyManager.getUserRoles(any(String.class))).thenReturn(new UserRole()); Errors errors = new Errors(); when(mockValidationErrorsException.getViolations(any(ViolationSeverity.class))).thenReturn(errors); doThrow(mockValidationErrorsException).when(mockUserService).updateUser(any(UserPayload.class), any(URI.class)); }
  38. 38. 38 .io Be more accurate than this code ? @RequestMapping(value = "/edit", method = RequestMethod.POST) public ModelAndView update(@RequestParam String resourceId, @ModelAttribute(PARTNER) UserPayload partner, BindingResult result, SessionStatus status) { boolean isTokenExists = partner.getTokensDefined(); if (partner.getUserSupplyTypesCodes() == null) partner.setUserSupplyTypesCodes(new HashSet<String>()); if (isTokenExists) partner.getUserSupplyTypesCodes().add("Identity"); UserRole partnerRole = currencyManager.getUserRoles(partner.getCode()); if (partnerRole.isIssuingUser()) partner.getUserSupplyTypesCodes().add("Issuance"); if (partnerRole.isLiabilityUser()) partner.getUserSupplyTypesCodes().add("Liability"); try { partnerService.updateUser(partner, URI.create(resourceId)); status.setComplete(); return new ModelAndView("redirect:/partners"); } catch (ValidationErrorsException e) { SpringErrorBindingHelper.populateErrors(result, e.getViolations(ViolationSeverity.ERROR)); ModelAndView mav = new ModelAndView("administration/partners/edit"); partnerRole = currencyManager.getUserRoles(partner.getCode()); mav.addObject(PARTNER_ROLE, partnerRole); mav.addObject(RESOURCE_ID, resourceId); populateReferenceData(mav); return mav; } }
  39. 39. 39 .io And this test? "Sending two JSON records to the messages REST API" should { "returns HTTP Status 200 and store two records to the messages repository" in { // Given val jsonRows = Json.parse( """[ |{ "id": "1234567890", "form": "SA300", "dispatchOn": "2013-11-22", "detailsId": "W0123456781234569"}, |{ "id": "1234567891", "form": "SA316A", "dispatchOn": "2013-11-23", "detailsId": "C0123456781234568"} |]""".stripMargin) // When verifyStatusCode(doPut(resource(s"/messages"), jsonRows), 200) // Then val messages = await(messageRepository.findAll) messages should have size 2 exactly(1, messages) should have ( 'recipient("1234567890"), 'body("SA300", DetailsId("W0123456781234569")), 'dispatchOn(new LocalDate(2013, 11, 22)) ) exactly(1, messages) should have ( 'recipient("1234567891"), 'body("SA316A", DetailsId("C0123456781234568")), 'validFrom(new LocalDate(2013, 11, 23)) ) } }
  40. 40. 40 .io Compared to this code ? def putMessages = Action.async(action.parser) { implicit request => withJsonBody[List[PrintSuppressionNotification]] { messages => if (messages == null || messages.isEmpty) throw new BadRequestException("No messages supplied") if (messages.size > maxMessages) throw new RequestEntityTooLargeException( s"${messages.size} items submitted, max $maxMessages allowed") val results = mongo.insertAllUnique( Future.sequence(results).map(_ => Results.Ok) } }
  41. 41. 41 .io Other projects' testimonials "Today however, my team told me the tests are more complex than the actual code. (This team is not the original team that wrote the code and unit tests. Therefore some unit tests take them by surprise. This current team is more senior and disciplined.) In my opinion, now that’s waste..." Richard Jacobs at Sogeti (Sogeti Nederland B.V.)
  42. 42. 42 .io Accurate = Small More code = more mistakes Ideal size of a test? ~ 3 lines Assumption, Action, Assertion "Less is more" [Ludwig Mies van der Rohe]
  43. 43. 43 .io Accurate = Readable We can trust what we understand Make your test easy to read Anyone would understand and validate it
  44. 44. 44 .io Accurate = Explicit and repeatable What You See is What Test Does Again and again the same No side-effects No hidden logic in helper No loops No random values
  45. 45. 45 .io Accurate = Traceable to a requirement When test fails, what stops working? Use business domain names use business stories names for objects, actions and results
  46. 46. 46 .io Accurate = Correct when fails If I comment out a line of code, does the test FAIL for the CORRECT reason? Does the test returns an error (exception) instead of a failure?
  48. 48. 48 .io
  50. 50. 50 .io Are you telling me I should have less tests ? Q [John Nolan]: "The thing I've found about TDD is that its takes time to get your tests set up and being naturally lazy I always want to write as little code as possible. The first thing I seem do is test my constructor has set all the properties but is this overkill? My question is to what level of granularity do you write you unit tests at? ..and is there a case of testing too much?" A [Kent Beck]: "I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence"
  52. 52. 52 .io Experience over Rules Writing CODE is MORE than applying RULES Reading CODE is MORE than reading BOOKS Common sense, Trial and Error
  53. 53. 53 .io The Sense of Time Test too complex to write ? Kill the test (if the code is clean) Test takes too long to understand? Kill the test (if the code is readable) Time (with TDD) < Time (without TDD) or you are doing something wrong!
  54. 54. 54 .io Cost / value trade-off Writing tests costs money Test marginal value > Test cost? Is this code critical? Can it possibly fail? Am I double-testing something covered? Cost (with TDD) < Cost (without TDD) or you are doing something wrong!
  55. 55. 55 .io How big is my code-base with TDD? How many test / code lines I am writing? Confidence on uncovered code? Size (Test) < Size (Code) Do not test code that cannot break Test / code ratio
  56. 56. 56 .io Test Focus Tests express WHAT to achieve from user's perspective Test WHAT can be measured What can I measure on this? public interface UserRepository { void addUser(String username, String first, String last); }
  57. 57. 57 .io Code confidence Code (not Test) tells HOW system works Code (with Test) gives confidence of correctness We are confident on what is clear + readable + repeatable
  58. 58. 58 .io Writing code - Martin Fowler "Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” [Martin Fowler - 1999 - Refactoring: Improving the Design of Existing Code]
  59. 59. 59 .io Best writers in history – Ernest Hemingway "In 1954, when Hemingway was awarded the Nobel Prize for Literature, it was for his mastery of the art of narrative […] He avoided complicated syntax. About 70 percent of the sentences are simple sentences—a childlike syntax without subordination."
  60. 60. 60 .io Reading = learning to Write Code Review Read A LOT of code Read good code, read bad code Comment, exchange ideas Challenge solutions
  61. 61. 61 .io Write, experiment, throw waste Write code, ask for feedback Read your own code … after days, weeks, months Measure progress Throw waste Keep test and code clean, ALWAYS
  62. 62. 62 .io Professional TDD Summary 1. TDD practices are not enough 2. Experience over Rules 3. Trade-off test time/cost and code benefit 4. Tests is WHAT to achieve 5. Code is confidence on DESIGN 6. Read code to become a better writer 7. Plan to throw test waste away
  63. 63. 63 .io
  64. 64. 64 .io TDD resources for reading and learning  Is TDD dead hangouts series  Unit-testing and waste management  Mockist vs. non-mockist TDD
  65. 65. 65 .io Code-review resources  Guido Van Rossum about Code Review @Google  Code Review @SAP  V. Subramaniam – About Code Review and Quality
  66. 66. Replay these slides: Try Gerrit on-line: Learn Gerrit: Follow my Blog: Learn more about code review 20% OFF Book discount for 33 User Conference Book CODE: dg7jnZ eBook CODE: Wi86Zh