$30 off During Our Annual Pro Sale. View Details »

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. Fake It While You Make It Kevin Murphy The Gnar

    Company @KEVIN_J_M
  2. Third-Party Dependencies @KEVIN_J_M

  3. @KEVIN_J_M

  4. @KEVIN_J_M

  5. 55 65 @KEVIN_J_M

  6. 55 65 MA @KEVIN_J_M

  7. Gnar Company Logo @KEVIN_J_M

  8. @KEVIN_J_M

  9. @KEVIN_J_M

  10. @KEVIN_J_M

  11. @KEVIN_J_M

  12. SWEATHR

  13. $11 Billion SWEATHR @KEVIN_J_M

  14. $22 Billion? SWEATHR @KEVIN_J_M

  15. SWEATHR Third-party dependency @KEVIN_J_M

  16. HTTP API @KEVIN_J_M

  17. @KEVIN_J_M

  18. class Location end @KEVIN_J_M

  19. class Location def sweater_weather? end end @KEVIN_J_M

  20. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) end end @KEVIN_J_M

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

    end end @KEVIN_J_M
  22. class Location def sweater_weather? uri = URI(conditions_url_for_zip_code) result = Net::HTTP.get(uri)

    response = JSON.parse(result) end end @KEVIN_J_M
  23. 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
  24. 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
  25. 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
  26. > 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
  27. > 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
  28. > 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
  29. Direct Interaction ✓ Con fi dence @KEVIN_J_M

  30. Direct Interaction ✓ Con fi dence ✓ Production parity @KEVIN_J_M

  31. Direct Interaction ✓ Con fi dence ✓ Production parity -

    Slow @KEVIN_J_M
  32. Direct Interaction ✓ Con fi dence ✓ Production parity -

    Slow - Non-deterministic @KEVIN_J_M
  33. Direct Interaction ✓ Con fi dence ✓ Production parity -

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

    Slow - Non-deterministic - Dependency required - Rate limiting @KEVIN_J_M
  35. Why Direct Interaction? ★ Unfamiliar with dependency @KEVIN_J_M

  36. Why Direct Interaction? ★ Unfamiliar with dependency ★ Low barrier

    to interact @KEVIN_J_M
  37. Stub

  38. before do api_response = { current_observation: { feelslike_f: “55.0” }

    }.to_json end @KEVIN_J_M
  39. 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
  40. 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
  41. Stub ✓ Deterministic @KEVIN_J_M

  42. Stub ✓ Deterministic ✓ Bypass dependency @KEVIN_J_M

  43. Stub ✓ Deterministic ✓ Bypass dependency ✓ Isolated unit @KEVIN_J_M

  44. Stub ✓ Deterministic ✓ Bypass dependency ✓ Isolated unit -

    Must know response structure @KEVIN_J_M
  45. Stub ✓ Deterministic ✓ Bypass dependency ✓ Isolated unit -

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

    Must know response structure - Stub response vs. reality - Possibly verbose @KEVIN_J_M
  47. Why Stub? ★ Stable interface @KEVIN_J_M

  48. Why Stub? ★ Stable interface ★ Small response @KEVIN_J_M

  49. Fake

  50. class FakeWeather < Sinatra::Base end @KEVIN_J_M

  51. class FakeWeather < Sinatra::Base get “/api/:key/conditions/q/:zip_code.json” do end end @KEVIN_J_M

  52. class FakeWeather < Sinatra::Base get “/api/:key/conditions/q/:zip_code.json” do json current_observation: {

    feelslike_f: "56.0" } end end @KEVIN_J_M
  53. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) end @KEVIN_J_M
  54. 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
  55. 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
  56. 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
  57. Fake ✓ Full stack test @KEVIN_J_M

  58. Fake ✓ Full stack test ✓ Local, deployable sandbox @KEVIN_J_M

  59. Fake ✓ Full stack test ✓ Local, deployable sandbox ✓

    Limited noise in setup @KEVIN_J_M
  60. Fake ✓ Full stack test ✓ Local, deployable sandbox ✓

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

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

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

  64. Why Fake? ★ Need con fi dence in communication ★

    Multi-step interaction @KEVIN_J_M
  65. Fixture

  66. it "is time to break out the sweater" do location

    = Sweathr::Location.new( zip_code: “02108”) end @KEVIN_J_M
  67. 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
  68. 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
  69. Fixture ✓ Truth at moment in time @KEVIN_J_M

  70. Fixture ✓ Truth at moment in time ✓ Complete picture

    @KEVIN_J_M
  71. Fixture ✓ Truth at moment in time ✓ Complete picture

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

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

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

  75. Why Fixture? ★ Need complete response ★ Dependency accessible @KEVIN_J_M

  76. Why Fixture? ★ Need complete response ★ Dependency accessible ★

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

    Fixture creation limits impact on dependency ★ Can refresh regularly @KEVIN_J_M
  78. @KEVIN_J_M STOP

  79. @KEVIN_J_M

  80. 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
  81. 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
  82. 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
  83. 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
  84. API Client

  85. module Sweathr module Weather class Api end end end @KEVIN_J_M

  86. module Sweathr module Weather class Api def current_conditions(zip:) end end

    end end @KEVIN_J_M
  87. 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
  88. 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
  89. it "retrieves the current conditions" do VCR.use_cassette("temp_needs_sweater") do end end

    @KEVIN_J_M
  90. it "retrieves the current conditions" do VCR.use_cassette("temp_needs_sweater") do result =

    api.current_conditions(zip: “02108") end end @KEVIN_J_M
  91. 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
  92. Data Representation

  93. module Sweathr module Weather class CurrentConditions end end end @KEVIN_J_M

  94. module Sweathr module Weather class CurrentConditions def initialize(results) @results =

    results end end end end @KEVIN_J_M
  95. module Sweathr module Weather class CurrentConditions def initialize(results) @results =

    results end def feels_like_f end end end end @KEVIN_J_M
  96. 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
  97. 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
  98. describe "#feels_like_f" do it “gives the feels like temp in

    Fahrenheit" do end end @KEVIN_J_M
  99. 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
  100. 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
  101. 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
  102. 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
  103. 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
  104. Domain

  105. module Sweathr module Weather end end @KEVIN_J_M

  106. module Sweathr module Weather def self.client @client ||= Api.new end

    end end @KEVIN_J_M
  107. module Sweathr module Weather def self.client @client ||= Api.new end

    def self.current_conditions(zip_code:) end end end @KEVIN_J_M
  108. 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
  109. Implementation

  110. class Location def sweater_weather? end end @KEVIN_J_M

  111. class Location def sweater_weather? current = Sweathr::Weather.current_conditions( zip_code: @zip_code) end

    end @KEVIN_J_M
  112. class Location def sweater_weather? current = Sweathr::Weather.current_conditions( zip_code: @zip_code) current.feels_like_f.between?(55,

    65) end end @KEVIN_J_M
  113. module Sweathr module Weather def self.client @client ||= Api.new end

    end end @KEVIN_J_M
  114. module Sweathr module Weather def self.client @client ||= Api.new end

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

    def self.client=(client) @client = client end end end @KEVIN_J_M
  116. Fake Client

  117. class FakeWeatherClient end @KEVIN_J_M

  118. class FakeWeatherClient def current_conditions(zip:) end end @KEVIN_J_M

  119. class FakeWeatherClient def current_conditions(zip:) { "current_observation" => { "feelslike_f" =>

    @results[zip_code] } } end end @KEVIN_J_M
  120. 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
  121. 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
  122. Test Mode

  123. module Sweathr module Testing end end @KEVIN_J_M

  124. module Sweathr module Testing def self.enable! end end end @KEVIN_J_M

  125. module Sweathr module Testing def self.enable! Sweathr::Weather.client = FakeWeatherClient.new end

    end end @KEVIN_J_M
  126. module Sweathr module Testing def self.enable! Sweathr::Weather.client = FakeWeatherClient.new end

    def self.disable! end end end @KEVIN_J_M
  127. 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
  128. Testing

  129. it "is time to break out the sweater" do end

    @KEVIN_J_M
  130. it "is time to break out the sweater" do Sweathr::Testing.enable!

    end @KEVIN_J_M
  131. 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
  132. 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
  133. 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
  134. 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
  135. Test Mode ✓ Speed @KEVIN_J_M

  136. Test Mode ✓ Speed ✓ State control @KEVIN_J_M

  137. Test Mode ✓ Speed ✓ State control ✓ Metadata tracking

    @KEVIN_J_M
  138. Test Mode ✓ Speed ✓ State control ✓ Metadata tracking

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

    - No dependency interaction - Test mode vs. reality @KEVIN_J_M
  140. Why Test Mode? ★ Have dependency coverage elsewhere @KEVIN_J_M

  141. Why Test Mode? ★ Have dependency coverage elsewhere ★ Need

    interaction with results @KEVIN_J_M
  142. Testing Third-Party Dependencies? @KEVIN_J_M

  143. DON’T* @KEVIN_J_M

  144. @KEVIN_J_M

  145. Client @KEVIN_J_M

  146. Client Data @KEVIN_J_M

  147. Client Data Domain @KEVIN_J_M

  148. Client Data Domain @KEVIN_J_M

  149. Client Data Domain @KEVIN_J_M

  150. Client Data Domain @KEVIN_J_M

  151. Client Data Domain @KEVIN_J_M

  152. @KEVIN_J_M

  153. Limit Interaction @KEVIN_J_M

  154. Limit Interaction Test Con fi dently @KEVIN_J_M

  155. Gnar Company Logo https://github.com/kevin-j-m/testing-services @KEVIN_J_M https://thegnar.co