Slide 1

Slide 1 text

Fake It While You Make It Kevin Murphy The Gnar Company @KEVIN_J_M

Slide 2

Slide 2 text

Third-Party Dependencies @KEVIN_J_M

Slide 3

Slide 3 text

@KEVIN_J_M

Slide 4

Slide 4 text

@KEVIN_J_M

Slide 5

Slide 5 text

55 65 @KEVIN_J_M

Slide 6

Slide 6 text

55 65 MA @KEVIN_J_M

Slide 7

Slide 7 text

Gnar Company Logo @KEVIN_J_M

Slide 8

Slide 8 text

@KEVIN_J_M

Slide 9

Slide 9 text

@KEVIN_J_M

Slide 10

Slide 10 text

@KEVIN_J_M

Slide 11

Slide 11 text

@KEVIN_J_M

Slide 12

Slide 12 text

SWEATHR

Slide 13

Slide 13 text

$11 Billion SWEATHR @KEVIN_J_M

Slide 14

Slide 14 text

$22 Billion? SWEATHR @KEVIN_J_M

Slide 15

Slide 15 text

SWEATHR Third-party dependency @KEVIN_J_M

Slide 16

Slide 16 text

HTTP API @KEVIN_J_M

Slide 17

Slide 17 text

@KEVIN_J_M

Slide 18

Slide 18 text

class Location end @KEVIN_J_M

Slide 19

Slide 19 text

class Location def sweater_weather? end end @KEVIN_J_M

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

> 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

Slide 27

Slide 27 text

> 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

Slide 28

Slide 28 text

> 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

Slide 29

Slide 29 text

Direct Interaction ✓ Con fi dence @KEVIN_J_M

Slide 30

Slide 30 text

Direct Interaction ✓ Con fi dence ✓ Production parity @KEVIN_J_M

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Why Direct Interaction? ★ Unfamiliar with dependency @KEVIN_J_M

Slide 36

Slide 36 text

Why Direct Interaction? ★ Unfamiliar with dependency ★ Low barrier to interact @KEVIN_J_M

Slide 37

Slide 37 text

Stub

Slide 38

Slide 38 text

before do api_response = { current_observation: { feelslike_f: “55.0” } }.to_json end @KEVIN_J_M

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

Stub ✓ Deterministic @KEVIN_J_M

Slide 42

Slide 42 text

Stub ✓ Deterministic ✓ Bypass dependency @KEVIN_J_M

Slide 43

Slide 43 text

Stub ✓ Deterministic ✓ Bypass dependency ✓ Isolated unit @KEVIN_J_M

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Why Stub? ★ Stable interface @KEVIN_J_M

Slide 48

Slide 48 text

Why Stub? ★ Stable interface ★ Small response @KEVIN_J_M

Slide 49

Slide 49 text

Fake

Slide 50

Slide 50 text

class FakeWeather < Sinatra::Base end @KEVIN_J_M

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Fake ✓ Full stack test @KEVIN_J_M

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Why Fake? ★ Need con fi dence in communication @KEVIN_J_M

Slide 64

Slide 64 text

Why Fake? ★ Need con fi dence in communication ★ Multi-step interaction @KEVIN_J_M

Slide 65

Slide 65 text

Fixture

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Fixture ✓ Truth at moment in time @KEVIN_J_M

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

Fixture ✓ Truth at moment in time ✓ Complete picture - Mystery guest - Snapshot in time - Requires periodic system access @KEVIN_J_M

Slide 74

Slide 74 text

Why Fixture? ★ Need complete response @KEVIN_J_M

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

Why Fixture? ★ Need complete response ★ Dependency accessible ★ Fixture creation limits impact on dependency ★ Can refresh regularly @KEVIN_J_M

Slide 78

Slide 78 text

@KEVIN_J_M STOP

Slide 79

Slide 79 text

@KEVIN_J_M

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

API Client

Slide 85

Slide 85 text

module Sweathr module Weather class Api end end end @KEVIN_J_M

Slide 86

Slide 86 text

module Sweathr module Weather class Api def current_conditions(zip:) end end end end @KEVIN_J_M

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

it "retrieves the current conditions" do VCR.use_cassette("temp_needs_sweater") do end end @KEVIN_J_M

Slide 90

Slide 90 text

it "retrieves the current conditions" do VCR.use_cassette("temp_needs_sweater") do result = api.current_conditions(zip: “02108") end end @KEVIN_J_M

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

Data Representation

Slide 93

Slide 93 text

module Sweathr module Weather class CurrentConditions end end end @KEVIN_J_M

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

describe "#feels_like_f" do it “gives the feels like temp in Fahrenheit" do end end @KEVIN_J_M

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

Domain

Slide 105

Slide 105 text

module Sweathr module Weather end end @KEVIN_J_M

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

Implementation

Slide 110

Slide 110 text

class Location def sweater_weather? end end @KEVIN_J_M

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

Fake Client

Slide 117

Slide 117 text

class FakeWeatherClient end @KEVIN_J_M

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

Test Mode

Slide 123

Slide 123 text

module Sweathr module Testing end end @KEVIN_J_M

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

Testing

Slide 129

Slide 129 text

it "is time to break out the sweater" do end @KEVIN_J_M

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

Test Mode ✓ Speed @KEVIN_J_M

Slide 136

Slide 136 text

Test Mode ✓ Speed ✓ State control @KEVIN_J_M

Slide 137

Slide 137 text

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

Slide 138

Slide 138 text

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

Slide 139

Slide 139 text

Test Mode ✓ Speed ✓ State control ✓ Metadata tracking - No dependency interaction - Test mode vs. reality @KEVIN_J_M

Slide 140

Slide 140 text

Why Test Mode? ★ Have dependency coverage elsewhere @KEVIN_J_M

Slide 141

Slide 141 text

Why Test Mode? ★ Have dependency coverage elsewhere ★ Need interaction with results @KEVIN_J_M

Slide 142

Slide 142 text

Testing Third-Party Dependencies? @KEVIN_J_M

Slide 143

Slide 143 text

DON’T* @KEVIN_J_M

Slide 144

Slide 144 text

@KEVIN_J_M

Slide 145

Slide 145 text

Client @KEVIN_J_M

Slide 146

Slide 146 text

Client Data @KEVIN_J_M

Slide 147

Slide 147 text

Client Data Domain @KEVIN_J_M

Slide 148

Slide 148 text

Client Data Domain @KEVIN_J_M

Slide 149

Slide 149 text

Client Data Domain @KEVIN_J_M

Slide 150

Slide 150 text

Client Data Domain @KEVIN_J_M

Slide 151

Slide 151 text

Client Data Domain @KEVIN_J_M

Slide 152

Slide 152 text

@KEVIN_J_M

Slide 153

Slide 153 text

Limit Interaction @KEVIN_J_M

Slide 154

Slide 154 text

Limit Interaction Test Con fi dently @KEVIN_J_M

Slide 155

Slide 155 text

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