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

Take the pain out of your TDD

Take the pain out of your TDD

My interpretation of TDD for Brighton Ruby Conf 2014.

Andy Pike

July 21, 2014
Tweet

More Decks by Andy Pike

Other Decks in Programming

Transcript

  1. TAKE THE
    PAIN OUT OF
    YOUR TDD

    View Slide

  2. ME
    @ANDYPIKE
    ANDYPIKE
    [email protected]

    View Slide

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

    View Slide

  4. View Slide

  5. View Slide

  6. MY SANDI METZ SLIDE !

    View Slide

  7. THIS TALK
    WAS PROPOSED
    BEFORE RAILSCONF!

    View Slide

  8. THIS TALK IS ABOUT
    MY INTERPRETATION

    View Slide

  9. THIS TALK IS NOT ABOUT
    TOOLS OR RULES

    View Slide

  10. TDD
    GIVES ME
    CONFIDENCE

    View Slide

  11. RED
    GREEN
    REFACTOR

    View Slide

  12. RED
    WRITE A TEST THAT FAILS

    View Slide

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

    View Slide

  14. REFACTOR
    WITH TESTS AT OUR BACK
    ITERATE OVER THE CODE TO
    IMPROVE IT WITH SMALL CHANGES

    View Slide

  15. TDD
    USED TO CAUSE ME
    PAIN

    View Slide

  16. 1. BRITTLE

    View Slide

  17. 2. TRYING
    TO DRIVE
    DESIGN

    View Slide

  18. 3. TOO
    MANY
    MOCKS

    View Slide

  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

    View Slide

  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

    View Slide

  21. SOUNDS PAINFUL HUH? !

    View Slide

  22. WHAT DO
    I DO
    NOW?

    View Slide

  23. WRITE TWO
    TYPES OF
    TESTS:

    View Slide

  24. 1. FEATURE TESTS

    View Slide

  25. WHOLE STACK
    TEST JUST ENOUGH

    View Slide

  26. PROVE BASICS ARE WORKING
    FAVOUR RACKTEST
    LOOSELY COUPLE WITH UI
    LEAVE DETAILS TO UNIT TESTS

    View Slide

  27. 2. UNIT TESTS

    View Slide

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

    View Slide

  29. INTERNAL API BOUNDARY
    IS METHODS YOUR CONTROLLERS CALL.
    WHEN TESTING, TREAT
    INTERNAL API AS A BLACK BOX.

    View Slide

  30. View Slide

  31. WHEN REFACTORING OUT CLASSES
    THEY DO NOT NEED THEIR OWN
    TESTS AS THEY ARE
    IMPLEMENTATION DETAIL

    View Slide

  32. WAT? !

    View Slide

  33. ADD TESTS WHEN NEEDED,
    NOT MANDATORY

    View Slide

  34. HOW DO YOU FIND BUGS?
    KEEP CLASSES AND METHODS SMALL
    USE THE STACK TRACE !

    View Slide

  35. A SILLY EXAMPLE

    View Slide

  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

    View Slide

  37. START WITH
    FEATURE TEST

    View Slide

  38. describe "FizzBuzz game" do
    it "shows the homepage" do
    visit root_path
    expect(page).to have_content(/fizzbuzz/i)
    end
    end

    View Slide

  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

    View Slide

  40. class NumbersController < ApplicationController
    def index
    @numbers = %w(1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz)
    end
    end

    View Slide

  41. TESTS ARE GREEN !

    View Slide

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

    View Slide

  43. DETAILS WITH
    UNIT TESTS

    View Slide

  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

    View Slide

  45. context "with a range of 1..1" do
    it "returns 1" do
    expect(subject.build_list(1..1)).to eq(["1"])
    end
    end

    View Slide

  46. class FizzBuzz
    def build_list(range)
    ["1"]
    end
    end

    View Slide

  47. context "with a range of 3..3" do
    it "returns Fizz" do
    expect(subject.build_list(3..3)).to eq(["Fizz"])
    end
    end

    View Slide

  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

    View Slide

  49. context "with a range of 5..5" do
    it "returns Buzz" do
    expect(subject.build_list(5..5)).to eq(["Buzz"])
    end
    end

    View Slide

  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

    View Slide

  51. context "with a range of 15..15" do
    it "returns FizzBuzz" do
    expect(subject.build_list(15..15)).to eq(["FizzBuzz"])
    end
    end

    View Slide

  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

    View Slide

  53. TESTS ARE GREEN !

    View Slide

  54. FREEDOM
    TO
    ✨ REFACTOR ✨

    View Slide

  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

    View Slide

  56. class FizzBuzzProcessor
    include Processable
    def message
    "FizzBuzz"
    end
    def match?
    number % 3 == 0 && number % 5 == 0
    end
    end

    View Slide

  57. THE POINT IS I:
    DIDN'T CHANGE TESTS
    DIDN'T ADD TESTS

    View Slide

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

    View Slide

  59. JOB DONE !

    View Slide

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

    View Slide

  61. DATABASES
    CLASSIC RAILS: DON'T MOCK DB
    BIGGER RAILS: MOCK REPOSITORY

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  65. SPEED:
    I USE SPRING
    CONTEXT TEST RUNS
    SMALL BEFORE BLOCKS
    TRY TO PARALLELISE TESTS

    View Slide

  66. EXTERNAL CALLS:
    DON'T CALL EXTERNAL SERVICES IN TESTS
    USE VCR GEM

    View Slide

  67. DESIGN:
    I DON'T DRIVE DESIGN
    THROUGH TESTS.
    I USE TESTS TO
    HIGHLIGHT DESIGN SMELLS.

    View Slide

  68. REFACTOR
    DRIVEN
    DESIGN

    View Slide

  69. NOW WE HAPPY !

    View Slide

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

    View Slide

  71. SUMMARY:
    TEST JUST ENOUGH
    LOOSELY COUPLED TESTS
    REFACTOR DRIVEN DESIGN
    UNIT OF WORK NOT CODE

    View Slide

  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

    View Slide

  73. THANK YOU
    @andypike

    View Slide