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

Fake It While You Make It

Fake It While You Make It

We all write code to interface with external systems, like a web service or a message queue. Can you confidently write tests without requiring the system as a dependency? How can you shield users of your code from the inner workings of the interface? Explore one attempt to answer these questions.

There’s no shortage of tools at your disposal to solve these problems. This talk will introduce some available options, provide guidance on when one approach may be more appropriate than another, and discuss how to use these tools together to ease the testing process.

Kevin Murphy

June 19, 2022
Tweet

More Decks by Kevin Murphy

Other Decks in Programming

Transcript

  1. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) feels_like = response.dig( “current_observation”, “feelslike_f”) end end @KEVIN_J_M
  2. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) feels_like = response.dig( “current_observation”, “feelslike_f”) feels_like.to_f.between?(55, 65) end end @KEVIN_J_M
  3. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108") expect(location.sweater_weather?).to be true end @KEVIN_J_M
  4. > rspec spec/sweathr/location_spec.rb Failures: 1) Sweathr::Location#sweater_weather? is time to break

    out the sweater Failure/Error: expect(weather.sweater_weather?).to be true expected true got false Finished in 0.36974 seconds ( fi les took 0.2812 seconds to load) 1 example, 1 failure 70° F @KEVIN_J_M
  5. > rspec spec/sweathr/location_spec.rb . Finished in 0.37186 seconds ( fi

    les took 0.48152 seconds to load) 1 example, 0 failures 63° F @KEVIN_J_M
  6. > rspec spec/sweathr/location_spec.rb Failures: 1) Sweathr::Location#sweater_weather? is time to break

    out the sweater Failure/Error: expect(weather.sweater_weather?).to be true expected true got false Finished in 0.36974 seconds ( fi les took 0.2812 seconds to load) 1 example, 1 failure 53° F @KEVIN_J_M
  7. Direct Interaction ✓ Con fi dence ✓ Production parity -

    Slow - Non-deterministic - Dependency required @KEVIN_J_M
  8. Direct Interaction ✓ Con fi dence ✓ Production parity -

    Slow - Non-deterministic - Dependency required - Rate limiting @KEVIN_J_M
  9. before do api_response = { current_observation: { feelslike_f: “55.0” }

    }.to_json stub_request(:get, url) .to_return(status: 200, body: api_response) end @KEVIN_J_M
  10. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108") expect(location.sweater_weather?).to be true end @KEVIN_J_M
  11. Stub ✓ Deterministic ✓ Bypass dependency ✓ Isolated unit -

    Must know response structure - Stub response vs. reality @KEVIN_J_M
  12. Stub ✓ Deterministic ✓ Bypass dependency ✓ Isolated unit -

    Must know response structure - Stub response vs. reality - Possibly verbose @KEVIN_J_M
  13. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) end @KEVIN_J_M
  14. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) Capybara::Discoball.spin(FakeWeather) do |s| end end @KEVIN_J_M
  15. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) Capybara::Discoball.spin(FakeWeather) do |s| location.endpoint_url = s.url end end @KEVIN_J_M
  16. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) Capybara::Discoball.spin(FakeWeather) do |s| location.endpoint_url = s.url expect(location.sweater_weather?).to be true end end @KEVIN_J_M
  17. Fake ✓ Full stack test ✓ Local, deployable sandbox ✓

    Limited noise in setup ✓ Control fake complexity @KEVIN_J_M
  18. Fake ✓ Full stack test ✓ Local, deployable sandbox ✓

    Limited noise in setup ✓ Control fake complexity - Maintain consistency @KEVIN_J_M
  19. Fake ✓ Full stack test ✓ Local, deployable sandbox ✓

    Limited noise in setup ✓ Control fake complexity - Maintain consistency - Test the fake @KEVIN_J_M
  20. Why Fake? ★ Need con fi dence in communication ★

    Multi-step interaction @KEVIN_J_M
  21. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) end @KEVIN_J_M
  22. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) VCR.use_cassette(“temp_needs_sweater") do end end @KEVIN_J_M
  23. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) VCR.use_cassette(“temp_needs_sweater") do expect(location.sweater_weather?).to be true end end @KEVIN_J_M
  24. Fixture ✓ Truth at moment in time ✓ Complete picture

    - Mystery guest - Snapshot in time @KEVIN_J_M
  25. Fixture ✓ Truth at moment in time ✓ Complete picture

    - Mystery guest - Snapshot in time - Requires periodic system access @KEVIN_J_M
  26. Why Fixture? ★ Need complete response ★ Dependency accessible ★

    Fixture creation limits impact on dependency @KEVIN_J_M
  27. Why Fixture? ★ Need complete response ★ Dependency accessible ★

    Fixture creation limits impact on dependency ★ Can refresh regularly @KEVIN_J_M
  28. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) feels_like = response.dig( “current_observation”, “feelslike_f”) feels_like.to_f.between?(55, 65) end end @KEVIN_J_M
  29. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) feels_like = response.dig( “current_observation”, “feelslike_f”) feels_like.to_f.between?(55, 65) end end 1 @KEVIN_J_M
  30. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) feels_like = response.dig( “current_observation”, “feelslike_f”) feels_like.to_f.between?(55, 65) end end 1 2 @KEVIN_J_M
  31. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) feels_like = response.dig( “current_observation”, “feelslike_f”) feels_like.to_f.between?(55, 65) end end 1 2 3 @KEVIN_J_M
  32. module Sweathr module Weather class Api def current_conditions(zip:) uri =

    URI( “#{auth_uri}/conditions/q/#{zip}.json”) end end end end @KEVIN_J_M
  33. module Sweathr module Weather class Api def current_conditions(zip:) uri =

    URI( “#{auth_uri}/conditions/q/#{zip}.json”) JSON.parse(Net::HTTP.get(uri)) end end end end @KEVIN_J_M
  34. it "retrieves the current conditions" do VCR.use_cassette("temp_needs_sweater") do result =

    api.current_conditions(zip: “02108") expect(result).to include( "current_observation" => a_hash_including( "feelslike_f" => a_kind_of(String) ) ) end end @KEVIN_J_M
  35. module Sweathr module Weather class CurrentConditions def initialize(results) @results =

    results end def feels_like_f value = @results.dig( "current_observation", “feelslike_f") end end end end @KEVIN_J_M
  36. module Sweathr module Weather class CurrentConditions def initialize(results) @results =

    results end def feels_like_f value = @results.dig( "current_observation", “feelslike_f") string_to_ fl oat(value) end end end end @KEVIN_J_M
  37. describe "#feels_like_f" do it “gives the feels like temp in

    Fahrenheit" do conditions = Sweathr::Weather::CurrentConditions .new(json) end end @KEVIN_J_M
  38. describe "#feels_like_f" do it “gives the feels like temp in

    Fahrenheit" do conditions = Sweathr::Weather::CurrentConditions .new(json) expect(conditions.feels_like_f).to eq 55.5 end end @KEVIN_J_M
  39. describe "#feels_like_f" do it “gives the feels like temp in

    Fahrenheit" do conditions = Sweathr::Weather::CurrentConditions .new(json) expect(conditions.feels_like_f).to eq 55.5 end context "with a non-numeric result";end end @KEVIN_J_M
  40. describe "#feels_like_f" do it “gives the feels like temp in

    Fahrenheit" do conditions = Sweathr::Weather::CurrentConditions .new(json) expect(conditions.feels_like_f).to eq 55.5 end context "with a non-numeric result";end context "with no current observation”;end end @KEVIN_J_M
  41. describe "#feels_like_f" do it "gives the feels like temp in

    Fahrenheit” do conditions = Sweathr::Weather::CurrentConditions .new(json) expect(conditions.feels_like_f).to eq 55.5 end context "with a non-numeric result";end context "with no current observation”;end context "with no feels like f temperature";end end @KEVIN_J_M
  42. module Sweathr module Weather def self.client @client ||= Api.new end

    def self.current_conditions(zip_code:) end end end @KEVIN_J_M
  43. module Sweathr module Weather def self.client @client ||= Api.new end

    def self.current_conditions(zip_code:) Sweathr::Weather::CurrentConditions.new( client.current_conditions(zip: zip_code)) end end end @KEVIN_J_M
  44. module Sweathr module Weather def self.client @client ||= Api.new end

    def self.client=(client) end end end @KEVIN_J_M
  45. module Sweathr module Weather def self.client @client ||= Api.new end

    def self.client=(client) @client = client end end end @KEVIN_J_M
  46. class FakeWeatherClient def current_conditions(zip:) { "current_observation" => { "feelslike_f" =>

    @results[zip_code] } } end def add_condition(zip_code:, temp_f:) end end @KEVIN_J_M
  47. class FakeWeatherClient def current_conditions(zip:) { "current_observation" => { "feelslike_f" =>

    @results[zip_code] } } end def add_condition(zip_code:, temp_f:) @results[zip_code] = temp_f end end @KEVIN_J_M
  48. module Sweathr module Testing def self.enable! Sweathr::Weather.client = FakeWeatherClient.new end

    def self.disable! Sweathr::Weather.client = Sweathr::Weather::Api.new end end end @KEVIN_J_M
  49. it "is time to break out the sweater" do Sweathr::Testing.enable!

    Sweathr::Weather.client.add_condition( zip_code: "02108", temp_f: “56.0") end @KEVIN_J_M
  50. it "is time to break out the sweater" do Sweathr::Testing.enable!

    Sweathr::Weather.client.add_condition( zip_code: "02108", temp_f: “56.0") location = Sweathr::Location.new( zip_code: “02108”) end @KEVIN_J_M
  51. it "is time to break out the sweater" do Sweathr::Testing.enable!

    Sweathr::Weather.client.add_condition( zip_code: "02108", temp_f: “56.0") location = Sweathr::Location.new( zip_code: “02108”) expect(location.sweater_weather?).to be true end @KEVIN_J_M
  52. it "is time to break out the sweater" do Sweathr::Testing.enable!

    Sweathr::Weather.client.add_condition( zip_code: "02108", temp_f: “56.0") location = Sweathr::Location.new( zip_code: “02108”) expect(location.sweater_weather?).to be true Sweathr::Testing.disable! end @KEVIN_J_M
  53. Test Mode ✓ Speed ✓ State control ✓ Metadata tracking

    - No dependency interaction @KEVIN_J_M
  54. Test Mode ✓ Speed ✓ State control ✓ Metadata tracking

    - No dependency interaction - Test mode vs. reality @KEVIN_J_M