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

Better Code Through Boring(er) Tests

Betsy Haibel
November 11, 2016

Better Code Through Boring(er) Tests

Your tests are annoying. Whenever you update your application code, you need to change a lot of them. You've built yourself some test macros, and some nice FactoryGirl factories, and installed this really cool matcher gem... and your tests are still annoying. What if you changed your application code instead? In this talk, you'll learn what "listening to your tests" means in the real world: no mysticism, no arrogant TDD purism, just a few practical refactorings you can use tomorrow at work.

Betsy Haibel

November 11, 2016
Tweet

More Decks by Betsy Haibel

Other Decks in Technology

Transcript

  1. literally no one loves testing all the time ‣not even

    me ‣it’s ok to admit it ‣the testing police are not coming for you @betsythemuffin
  2. GROUNDWORK WHAT WE’RE GONNA DO ABOUT THAT TODAY ▸ learn

    why we hate our tests :( ▸ learn why we fight about tests :( ▸ learn about ways we accidentally make our tests worse :( ▸ learn how to fix those! ▸ learn how to make our app code better at the same time! @betsythemuffin
  3. you write your tests with the same imperfect knowledge as

    you write all your code UNDERLYING REASONS @betsythemuffin
  4. WHY DON’T YOU TRY LISTENING TO YOUR TESTS? some jerk

    who likes TDD too much THIS IS THE THING THAT ALWAYS HAPPENS. CRIKEY. UNDERLYING REASONS @betsythemuffin
  5. 1. TESTS YOU HATE 2. ??? 3. CODE YOU DON’T

    HATE some jerk who likes TDD too much WE TRANSLATE, WITHOUT EXCESSIVE KINDNESS UNDERLYING REASONS @betsythemuffin
  6. UGH I THINK WE CAN FIX THIS IF WE JUST

    TRY. I PROMISE. a TDDer who is not communicating well yet WE TRANSLATE, WITH SOMEWHAT MORE EMPATHY @betsythemuffin
  7. some notes before we get started properly ‣bad code happens

    ‣bad tests happen ‣this is fine UNDERLYING REASONS @betsythemuffin
  8. everything I am about to show you I have done

    in production especially the parts I say are bad UNDERLYING REASONS @betsythemuffin
  9. WHY DO PEOPLE TEST PRIVATE METHODS? ▸ indirect results ▸

    expensive to test via public interface ▸ weird side effects elsewhere TESTING PRIVATE METHODS - THE PROBLEM @betsythemuffin
  10. ALTERNATIVES TO TESTING PRIVATE METHODS ▸ make the method public

    ▸ build more introspection logic ▸ extract a simple class TESTING PRIVATE METHODS - ALTERNATIVES @betsythemuffin
  11. def dogear DogearMaker.new(self).call end class DogearMaker def initialize(target) @target =

    target end def call # stuff end end TESTING PRIVATE METHODS - EXTRACT SIMPLE CLASS @betsythemuffin
  12. it’s ok to fix things later TESTING PRIVATE METHODS -

    DEFER THE DECISION @betsythemuffin
  13. class ClientTest < Minitest::Test def test_mobile_phones @client.phone_number = MOBILE_PHONE assert

    @client.mobile_phone? end def test_landline_phones @client.phone_number = LANDLINE_PHONE assert [email protected]_phone? end end DUPLICATE TESTS - THE PROBLEM @betsythemuffin
  14. def test_mobile_phones @client.phone_number = MOBILE_PHONE assert @client.mobile_phone? end it ‘can

    have a mobile phone‘ do subject.phone_number = MOBILE_PHONE expect(subject.mobile_phone?).to be(true) end DUPLICATE TESTS - THE PROBLEM @betsythemuffin
  15. class ClientTest < Minitest::Test def test_mobile_phones @client.phone_number = MOBILE_PHONE assert

    @client.mobile_phone? end def test_landline_phones @client.phone_number = LANDLINE_PHONE assert [email protected]_phone? end end DUPLICATE TESTS - THE PROBLEM @betsythemuffin
  16. class DonorrTest < Minitest::Test def test_mobile_phones @donorr.phone_number = MOBILE_PHONE assert

    @donorr.mobile_phone? end def test_landline_phones @donorr.phone_number = LANDLINE_PHONE assert [email protected]_phone? end end DUPLICATE TESTS - THE PROBLEM @betsythemuffin
  17. module HasPhoneNumberTest def test_mobile_phones @subjec.phone_number = MOBILE_PHONE assert @subjec.mobile_phone? end

    def test_landline_phones @subjec.phone_number = LANDLINE_PHONE assert [email protected]_phone? end end DUPLICATE TESTS - THE SHARED EXAMPLE GROUP SMELL @betsythemuffin
  18. class ClientTest < Minitest::Test include HasPhoneNumberTest end class CustomerTest <

    Minitest::Test include HasPhoneNumberTest end DUPLICATE TESTS - THE SHARED EXAMPLE GROUP SMELL @betsythemuffin
  19. class Client include HasPhoneNumber end class Customer include HasPhoneNumber end

    DUPLICATE TESTS - THE SHARED EXAMPLE GROUP SMELL @betsythemuffin
  20. class HasPhoneNumberTest < Minitest::Test class HasPhone include HasPhoneNumber end def

    test_landline_phones @has_phone = HasPhone.new @has_phone.phone_number = LANDLINE_PHONE assert !@has_phone.mobile_phone? end end DUPLICATE TESTS - THE DIRECT MODULE TEST SMELL @betsythemuffin
  21. class PhoneNumber attr_accessor :value def mobile_phone? # complicated lookup logic

    end end DUPLICATE TESTS - THE EXTRACT CLASS FIX @betsythemuffin
  22. class PieTest < Minitest::Test def test_pie_tastes_good @universe.invent @apple_seed = AppleSeed.new

    @orchard = Orchard.new @orchard.plant(@apple_seed) @orchard.water! let_much_time_pass @apple_tree = Orchard.find_tree_from(@apple_seed) # etc assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust? end end INVENTING THE UNIVERSE - THE PROBLEM @betsythemuffin
  23. class PieTest < Minitest::Test def test_pie_tastes_good # many other lines

    assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust? end end INVENTING THE UNIVERSE - THE PROBLEM @betsythemuffin
  24. class PieTest < Minitest::Test def test_pie_tastes_good @universe.invent @apple_seed = AppleSeed.new

    @orchard = Orchard.new @orchard.plant(@apple_seed) @orchard.water! let_much_time_pass @apple_tree = Orchard.find_tree_from(@apple_seed) # etc assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust? end end INVENTING THE UNIVERSE - THE PREMATURE FIX SMELL @betsythemuffin
  25. your tests use your application’s assumptions (unless you try very

    hard) INVENTING THE UNIVERSE - THE PREMATURE FIX SMELL @betsythemuffin
  26. wait until you have multiple tests INVENTING THE UNIVERSE -

    THE PREMATURE FIX SMELL @betsythemuffin
  27. class PieTest < Minitest::Test include ReliesOnUniverseExisting def setup invent_universe end

    end INVENTING THE UNIVERSE - SHARED CONTEXT SMELL @betsythemuffin
  28. WHAT ARE SHARED CONTEXTS GOOD FOR? ▸ database setup ▸

    inventing the universe INVENTING THE UNIVERSE - SHARED CONTEXT SMELL @betsythemuffin
  29. WHAT DO SHARED CONTEXTS HIDE FROM US? ▸ dependencies ▸

    (but that’s not the whole story) INVENTING THE UNIVERSE - SHARED CONTEXT SMELL @betsythemuffin
  30. # constructor: Pie.new(filling: PumpkinPieFilling.new) # factory method on class: Pie.bake_with(PumpkinPieFilling.new)

    # java-style factory class factory = PieFactory.new factory.add_filling(PumpkinPieFilling.new) @pie = factory.bake INVENTING THE UNIVERSE - THE PLAIN FACTORY FIX @betsythemuffin
  31. Business logic is too important to delegate to framework magic

    INVENTING THE UNIVERSE - THE PLAIN FACTORY FIX @betsythemuffin
  32. class PieTest < Minitest::Test def test_pie_tastes_good @universe.invent @apple_seed = AppleSeed.new

    @orchard = Orchard.new @orchard.plant(@apple_seed) @orchard.water! let_much_time_pass @apple_tree = Orchard.find_tree_from(@apple_seed) # etc assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust? end end INVENTING THE UNIVERSE - THE PLAIN FACTORY FIX @betsythemuffin
  33. @universe.invent @apple_seed = AppleSeed.new @orchard = Orchard.new @orchard.plant(@apple_seed) @orchard.water! let_much_time_pass

    @apple_tree = Orchard.find_tree_from(@apple_seed) # etc assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust? INVENTING THE UNIVERSE - THE PLAIN FACTORY FIX @orchard = Universe.with_orchard(Apple) @pie = Pie.from(@orchard.fruit) assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust?
  34. INVENTING THE UNIVERSE - THE PLAIN FACTORY FIX class PieTest

    < Minitest::Test def test_pie_tastes_good @orchard = Universe.with_orchard(Apple) @pie = Pie.from(@orchard.fruit) assert @pie.filling_set_properly? assert @pie.cinnamon_level > 6 assert @pie.super_flaky_crust? end end @betsythemuffin
  35. 1. TESTS YOU DON’T HATE 2. REFACTORING 3. CODE YOU

    DON’T HATE all “listening to your tests” means A CONCLUSION @betsythemuffin
  36. SHAMELESS PROMOTION TIME BETSY HAIBEL ▸ @betsythemuffin ▸ betsyhaibel.com ▸

    github.com/bhaibel ▸ roostify.com/jobs @betsythemuffin