Take the pain out of your TDD

Take the pain out of your TDD

My interpretation of TDD for Brighton Ruby Conf 2014.

196ab25f16dcfd37518a41ceb15e0da0?s=128

Andy Pike

July 21, 2014
Tweet

Transcript

  1. TAKE THE PAIN OUT OF YOUR TDD

  2. ME @ANDYPIKE ANDYPIKE ANDY@1MINUS1.COM

  3. MY FIRST CONFERENCE TALK POLITE APPLAUSE IS JUST FINE !

  4. None
  5. None
  6. MY SANDI METZ SLIDE !

  7. THIS TALK WAS PROPOSED BEFORE RAILSCONF!

  8. THIS TALK IS ABOUT MY INTERPRETATION

  9. THIS TALK IS NOT ABOUT TOOLS OR RULES

  10. TDD GIVES ME CONFIDENCE

  11. RED GREEN REFACTOR

  12. RED WRITE A TEST THAT FAILS

  13. GREEN MAKE THE TEST PASS ASAP AS SIMPLE AS POSSIBLE

    SHAMELESS GREEN
  14. REFACTOR WITH TESTS AT OUR BACK ITERATE OVER THE CODE

    TO IMPROVE IT WITH SMALL CHANGES
  15. TDD USED TO CAUSE ME PAIN

  16. 1. BRITTLE

  17. 2. TRYING TO DRIVE DESIGN

  18. 3. TOO MANY MOCKS

  19. describe Account do describe "#register" do context "when params are

    valid" do let(:params) { {:name => "Andy"} } let(:user) { double } before { allow(User).to receive(:new) { user } } it "saves the user" do expect(user).to receive(:save) { true } subject.register(params) end it "returns :saved" do allow(user).to receive(:save) { true } expect(subject.register(params)).to eq(:saved) end end end end
  20. describe Account do describe "#register" do context "when params are

    valid" do let(:params) { {:name => "Andy"} } let(:user) { double } before { allow(User).to receive(:new) { user } } it "saves the user" do expect(user).to receive(:save) { true } subject.register(params) end it "returns :saved" do allow(user).to receive(:save) { true } expect(subject.register(params)).to eq(:saved) end end end end
  21. SOUNDS PAINFUL HUH? !

  22. WHAT DO I DO NOW?

  23. WRITE TWO TYPES OF TESTS:

  24. 1. FEATURE TESTS

  25. WHOLE STACK TEST JUST ENOUGH

  26. PROVE BASICS ARE WORKING FAVOUR RACKTEST LOOSELY COUPLE WITH UI

    LEAVE DETAILS TO UNIT TESTS
  27. 2. UNIT TESTS

  28. THE DETAILS. UNIT OF WORK NOT UNIT OF CODE.

  29. INTERNAL API BOUNDARY IS METHODS YOUR CONTROLLERS CALL. WHEN TESTING,

    TREAT INTERNAL API AS A BLACK BOX.
  30. None
  31. WHEN REFACTORING OUT CLASSES THEY DO NOT NEED THEIR OWN

    TESTS AS THEY ARE IMPLEMENTATION DETAIL
  32. WAT? !

  33. ADD TESTS WHEN NEEDED, NOT MANDATORY

  34. HOW DO YOU FIND BUGS? KEEP CLASSES AND METHODS SMALL

    USE THE STACK TRACE !
  35. A SILLY EXAMPLE

  36. FIZZBUZZ Print all numbers in a range. Multiples of 3

    print "Fizz" instead of the number. Multiples of 5 print "Buzz". Multiples of both 3 and 5 print "FizzBuzz". git.io/X5jVWA
  37. START WITH FEATURE TEST

  38. describe "FizzBuzz game" do it "shows the homepage" do visit

    root_path expect(page).to have_content(/fizzbuzz/i) end end
  39. context "valid input" do it "shows FizzBuzz numbers between a

    given range" do visit root_path fill_in "Min", :with => "1" fill_in "Max", :with => "10" click_on "Generate" expect(page).to have_content( "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz" ) end end
  40. class NumbersController < ApplicationController def index @numbers = %w(1 2

    Fizz 4 Buzz Fizz 7 8 Fizz Buzz) end end
  41. TESTS ARE GREEN !

  42. class NumbersController < ApplicationController def index @numbers = FizzBuzz.build_list(min..max) end

    ...
  43. DETAILS WITH UNIT TESTS

  44. describe FizzBuzz do describe "#build_list" do context "with a range

    of 1..15" do list = %w(1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz) it "returns #{list}" do expect(subject.build_list(1..15)).to eq(list) end end end end
  45. context "with a range of 1..1" do it "returns 1"

    do expect(subject.build_list(1..1)).to eq(["1"]) end end
  46. class FizzBuzz def build_list(range) ["1"] end end

  47. context "with a range of 3..3" do it "returns Fizz"

    do expect(subject.build_list(3..3)).to eq(["Fizz"]) end end
  48. class FizzBuzz def build_list(range) [].tap do |list| range.each do |n|

    if n % 3 == 0 list << "Fizz" else list << n.to_s end end end end end
  49. context "with a range of 5..5" do it "returns Buzz"

    do expect(subject.build_list(5..5)).to eq(["Buzz"]) end end
  50. class FizzBuzz def build_list(range) [].tap do |list| range.each do |n|

    if n % 3 == 0 list << "Fizz" elsif n % 5 == 0 list << "Buzz" else list << n.to_s end end end end end
  51. context "with a range of 15..15" do it "returns FizzBuzz"

    do expect(subject.build_list(15..15)).to eq(["FizzBuzz"]) end end
  52. class FizzBuzz def build_list(range) [].tap do |list| range.each do |n|

    if n % 3 == 0 && n % 5 == 0 list << "FizzBuzz" elsif n % 3 == 0 list << "Fizz" elsif n % 5 == 0 list << "Buzz" else list << n.to_s end end end end end
  53. TESTS ARE GREEN !

  54. FREEDOM TO ✨ REFACTOR ✨

  55. class FizzBuzz def build_list(range) [].tap do |list| range.map{ |n| Number.new(n)

    }.each do |n| processors.each do |processor| processor.new(list, n).process end end end end def processors [ FizzBuzzProcessor, FizzProcessor, BuzzProcessor, DefaultProcessor ] end end
  56. class FizzBuzzProcessor include Processable def message "FizzBuzz" end def match?

    number % 3 == 0 && number % 5 == 0 end end
  57. THE POINT IS I: DIDN'T CHANGE TESTS DIDN'T ADD TESTS

  58. TESTS NOT COUPLED TO IMPLEMENTATION. WE CAN REFACTOR WITHOUT PAIN.

  59. JOB DONE !

  60. MOCKS: I AVOID MOCKS EXCEPT: WHEN USING ALREADY TESTED API

    WHEN SETUP IS COMPLEX
  61. DATABASES CLASSIC RAILS: DON'T MOCK DB BIGGER RAILS: MOCK REPOSITORY

  62. AN API CALL SHOULD EITHER CHANGE STATE OR ANSWER A

    QUESTION
  63. describe Account do describe "#register" do context "when params are

    valid" do let(:params) { {:name => "Andy"} } let(:user) { double } before { allow(User).to receive(:new) { user } } it "saves the user" do expect(user).to receive(:save) { true } subject.register(params) end it "returns :saved" do allow(user).to receive(:save) { true } expect(subject.register(params)).to eq(:saved) end end end end
  64. describe Account do describe "#register" do context "when params are

    valid" do let(:params) { {:name => "Andy"} } it "saves the user" do subject.register(params) expect(User.count).to eq(1) end it "returns :saved" do expect(subject.register(params)).to eq(:saved) end end end end
  65. SPEED: I USE SPRING CONTEXT TEST RUNS SMALL BEFORE BLOCKS

    TRY TO PARALLELISE TESTS
  66. EXTERNAL CALLS: DON'T CALL EXTERNAL SERVICES IN TESTS USE VCR

    GEM
  67. DESIGN: I DON'T DRIVE DESIGN THROUGH TESTS. I USE TESTS

    TO HIGHLIGHT DESIGN SMELLS.
  68. REFACTOR DRIVEN DESIGN

  69. NOW WE HAPPY !

  70. TRADE OFFS AS DEVELOPERS WE ARE ALWAYS MAKING TRADE OFFS

  71. SUMMARY: TEST JUST ENOUGH LOOSELY COUPLED TESTS REFACTOR DRIVEN DESIGN

    UNIT OF WORK NOT CODE
  72. CREDITS ▸ Ian Cooper - TDD, where did it all

    go wrong? vimeo.com/68375232 ▸ Katrina Owen - Therapeutic Refactoring youtu.be/J4dlF0kcThQ ▸ Sandi Metz/Matt Wynne - #POODL Course kickstartacademy.io
  73. THANK YOU @andypike