78
Effectively Testing Services Neal Kemp

Effectively Testing Services - Burlington Ruby Conf

Embed Size (px)

DESCRIPTION

How to test external services in a ruby application

Citation preview

Page 1: Effectively Testing Services - Burlington Ruby Conf

Effectively Testing Services

Neal Kemp

Page 2: Effectively Testing Services - Burlington Ruby Conf

$ whoami Iowa native

Now: Californian

Software Developer

Ruby, Rails, Javascript, etc

Page 3: Effectively Testing Services - Burlington Ruby Conf
Page 4: Effectively Testing Services - Burlington Ruby Conf
Page 5: Effectively Testing Services - Burlington Ruby Conf
Page 6: Effectively Testing Services - Burlington Ruby Conf

what, why & how of testing services

Page 7: Effectively Testing Services - Burlington Ruby Conf

NOT Building testable services

Page 8: Effectively Testing Services - Burlington Ruby Conf

NOT Test-driven development

(necessarily)  

Page 9: Effectively Testing Services - Burlington Ruby Conf

… and because I don’t want @dhh to rage

Page 10: Effectively Testing Services - Burlington Ruby Conf

what

Page 11: Effectively Testing Services - Burlington Ruby Conf

What is a service?

Internal “SOA”

Page 12: Effectively Testing Services - Burlington Ruby Conf

Any time you make an HTTP request to an endpoint in another repository

Page 13: Effectively Testing Services - Burlington Ruby Conf

why

Page 14: Effectively Testing Services - Burlington Ruby Conf

Why are services important? Build faster

Makes scaling easier

Use them on virtually every application

Increasingly prevalent

Page 15: Effectively Testing Services - Burlington Ruby Conf

Services are critical to modern Rails development

Page 16: Effectively Testing Services - Burlington Ruby Conf

Why is testing services important? You (should) test everything else

Services compose crucial features

You may encounter problems…

Page 17: Effectively Testing Services - Burlington Ruby Conf

Internal API Sometimes null responses

Inconsistencies

Catastrophe

Page 18: Effectively Testing Services - Burlington Ruby Conf

Okay? But what about external APIs?

Page 19: Effectively Testing Services - Burlington Ruby Conf
Page 20: Effectively Testing Services - Burlington Ruby Conf

{"id": 24}{"code": "ANA"}

Page 21: Effectively Testing Services - Burlington Ruby Conf

"goals":[ { "per":"1", "ta":"CGY", "et":"14:11", "st":"Wrist Shot" }, { "per":"2", "ta":"ANA", "et":"11:12", "st":"Backhand" } ]

"goals": { "per":"1", "ta":"CGY", "et":"14:11", "st":"Wrist Shot" }

Page 22: Effectively Testing Services - Burlington Ruby Conf

No versioning!

Page 23: Effectively Testing Services - Burlington Ruby Conf
Page 24: Effectively Testing Services - Burlington Ruby Conf

Snapchat Client Haphazard documentation

What are the requests?

Bizarre obfuscation

github.com/nneal/snapcat

Page 25: Effectively Testing Services - Burlington Ruby Conf

how

Page 26: Effectively Testing Services - Burlington Ruby Conf

What is different about services? External network requests

You don’t own the code

Page 27: Effectively Testing Services - Burlington Ruby Conf
Page 28: Effectively Testing Services - Burlington Ruby Conf

On an airplane…

Failure is bad!

No network requests

Page 29: Effectively Testing Services - Burlington Ruby Conf

Don’t interact with services from test environment* **

Page 30: Effectively Testing Services - Burlington Ruby Conf

* Includes “dummy” APIs

Page 31: Effectively Testing Services - Burlington Ruby Conf

** Using pre-recorded responses is okay

Page 32: Effectively Testing Services - Burlington Ruby Conf

Assuming: Rails, rspec

Page 33: Effectively Testing Services - Burlington Ruby Conf

Time to stub!

Page 34: Effectively Testing Services - Burlington Ruby Conf

Built-in Stubbing Typhoeus

Faraday

Excon

Page 35: Effectively Testing Services - Burlington Ruby Conf

Simplify.

Page 36: Effectively Testing Services - Burlington Ruby Conf

gem 'webmock'

Page 37: Effectively Testing Services - Burlington Ruby Conf

ENV['RAILS_ENV'] ||= 'test'require File.expand_path('../../config/environment', __FILE__)require 'rspec/autorun'require 'rspec/rails’Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)RSpec.configure do |config| config.infer_base_class_for_anonymous_controllers = false config.order = 'random’endWebMock.disable_net_connect!

spec/spec_helper.rb

Page 38: Effectively Testing Services - Burlington Ruby Conf
Page 39: Effectively Testing Services - Burlington Ruby Conf

module FacebookWrapper def self.user_id(username) user_data(username)['id'] end def self.user_data(username) JSON.parse( open("https://graph.facebook.com/#{username}").read ) endend

lib/facebook_wrapper.rb

Page 40: Effectively Testing Services - Burlington Ruby Conf

require 'facebook_wrapper'

config/intializers/facebook_wrapper.rb

Page 41: Effectively Testing Services - Burlington Ruby Conf

require 'spec_helper'describe FacebookWrapper, '.user_link' do it 'retrieves user link' do stub_request(:get, 'https://graph.facebook.com/arjun'). to_return( status: 200, headers: {}, body: '{ "id": "7901103","first_name": "Arjun", "locale": "en_US","username": "Arjun" }' ) user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' endend

spec/lib/facebook_wrapper_spec.rb

Page 42: Effectively Testing Services - Burlington Ruby Conf

require 'spec_helper'describe FacebookWrapper, '.user_link' do it 'retrieves user link' do stub_request(:get, 'https://graph.facebook.com/arjun'). to_return( status: 200, headers: {}, body: '{ "id": "7901103","first_name": "Arjun", "locale": "en_US","username": "Arjun" }' ) user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' endend

spec/lib/facebook_wrapper_spec.rb

Page 43: Effectively Testing Services - Burlington Ruby Conf

require 'spec_helper'describe FacebookWrapper, '.user_link' do it 'retrieves user link' do stub_request(:get, 'https://graph.facebook.com/arjun'). to_return( status: 200, headers: {}, body: '{ "id": "7901103","first_name": "Arjun", "locale": "en_US","username": "Arjun" }' ) user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' endend

spec/lib/facebook_wrapper_spec.rb

Page 44: Effectively Testing Services - Burlington Ruby Conf

require 'spec_helper'describe FacebookWrapper, '.user_link' do it 'retrieves user link' do stub_request(:get, 'https://graph.facebook.com/arjun'). to_return( status: 200, headers: {}, body: '{ "id": "7901103","first_name": "Arjun", "locale": "en_US","username": "Arjun" }' ) user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' endend

spec/lib/facebook_wrapper_spec.rb

Page 45: Effectively Testing Services - Burlington Ruby Conf

Even Better No network requests

Fast!

No intermittent failure

Page 46: Effectively Testing Services - Burlington Ruby Conf

Mock-Services AWS

FB graph mock

OmniAuth

Etc…

Page 47: Effectively Testing Services - Burlington Ruby Conf

gem 'fb_graph-mock'

Page 48: Effectively Testing Services - Burlington Ruby Conf

ENV['RAILS_ENV'] ||= 'test'require File.expand_path('../../config/environment', __FILE__)require 'rspec/autorun'require 'rspec/rails’require 'fb_graph/mock' Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)RSpec.configure do |config| config.infer_base_class_for_anonymous_controllers = false config.order = 'random' config.include FbGraph::MockendWebMock.disable_net_connect!

spec/spec_helper.rb

Page 49: Effectively Testing Services - Burlington Ruby Conf

describe FacebookWrapper, '.user_link' do it 'retrieves user link' do mock_graph :get, 'arjun', 'users/arjun_public' do user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' end endend

spec/lib/facebook_wrapper_spec.rb

Page 50: Effectively Testing Services - Burlington Ruby Conf

describe FacebookWrapper, '.user_link' do it 'retrieves user link' do mock_graph :get, 'arjun', 'users/arjun_public' do user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' end endend

spec/lib/facebook_wrapper_spec.rb

Page 51: Effectively Testing Services - Burlington Ruby Conf

Even Better Already stubbed for you

Pre-recorded responses (sometimes)

Don’t need to know API endpoints

Page 52: Effectively Testing Services - Burlington Ruby Conf

gem 'sham_rack'

Page 53: Effectively Testing Services - Burlington Ruby Conf

gem 'sinatra'

Page 54: Effectively Testing Services - Burlington Ruby Conf

ENV['RAILS_ENV'] ||= 'test'require File.expand_path('../../config/environment', __FILE__)require 'rspec/autorun'require 'rspec/rails’Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)RSpec.configure do |config| config.infer_base_class_for_anonymous_controllers = false config.order = 'random’endWebMock.disable_net_connect!

spec/spec_helper.rb

Page 55: Effectively Testing Services - Burlington Ruby Conf

ShamRack.at('graph.facebook.com', 443).sinatra do get '/:username' do %Q|{ "id": "7901103", "name": "Arjun Banker", "first_name": "Arjun", "last_name": "Banker", "link": "http://www.facebook.com/#{params[:username]}", "location": { "id": 114952118516947, "name": "San Francisco, California" }, "gender": "male" }| endend

spec/support/fake_facebook.rb

Page 56: Effectively Testing Services - Burlington Ruby Conf

ShamRack.at('graph.facebook.com', 443).sinatra do get '/:username' do %Q|{ "id": "7901103", "name": "Arjun Banker", "first_name": "Arjun", "last_name": "Banker", "link": "http://www.facebook.com/#{params[:username]}", "location": { "id": 114952118516947, "name": "San Francisco, California" }, "gender": "male" }| endend

spec/support/fake_facebook.rb

Page 57: Effectively Testing Services - Burlington Ruby Conf

ShamRack.at('graph.facebook.com', 443).sinatra do get '/:username' do %Q|{ "id": "7901103", "name": "Arjun Banker", "first_name": "Arjun", "last_name": "Banker", "link": "http://www.facebook.com/#{params[:username]}", "location": { "id": 114952118516947, "name": "San Francisco, California" }, "gender": "male" }| endend

spec/support/fake_facebook.rb

Page 58: Effectively Testing Services - Burlington Ruby Conf

ShamRack.at('graph.facebook.com', 443).sinatra do get '/:username' do %Q|{ "id": "7901103", "name": "Arjun Banker", "first_name": "Arjun", "last_name": "Banker", "link": "http://www.facebook.com/#{params[:username]}", "location": { "id": 114952118516947, "name": "San Francisco, California" }, "gender": "male" }| endend

spec/support/fake_facebook.rb

Page 59: Effectively Testing Services - Burlington Ruby Conf

describe FacebookWrapper, '.user_link' do it 'retrieves user link' do user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103’ endend

spec/lib/facebook_wrapper_spec.rb

Page 60: Effectively Testing Services - Burlington Ruby Conf

Even Better Dynamic

Expressive

Readable

Page 61: Effectively Testing Services - Burlington Ruby Conf

gem 'vcr'

Page 62: Effectively Testing Services - Burlington Ruby Conf

ENV['RAILS_ENV'] ||= 'test'require File.expand_path('../../config/environment', __FILE__)require 'rspec/autorun'require 'rspec/rails’Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }ActiveRecord::Migration.check_pending! if defined?(ActiveRecord::Migration)RSpec.configure do |config| config.infer_base_class_for_anonymous_controllers = false config.order = 'random’endWebMock.disable_net_connect!VCR.configure do |c| c.cassette_library_dir = 'spec/fixtures/vcr_cassettes' c.hook_into :webmock end spec/spec_helper.rb

Page 63: Effectively Testing Services - Burlington Ruby Conf

describe FacebookWrapper, '.user_link' do it 'retrieves user link' do VCR.use_cassette('fb_user_arjun') do user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' end endend

spec/lib/facebook_wrapper_spec.rb

Page 64: Effectively Testing Services - Burlington Ruby Conf

describe FacebookWrapper, '.user_link' do it 'retrieves user link' do VCR.use_cassette('fb_user_arjun') do user_id = FacebookWrapper.user_id('arjun') expect(user_id).to eq '7901103' end endend

spec/lib/facebook_wrapper_spec.rb

Page 65: Effectively Testing Services - Burlington Ruby Conf

Even Better Record API automatically

Replay responses without network

Verify responses

Page 66: Effectively Testing Services - Burlington Ruby Conf

Additional Build Process Runs outside normal test mode

Rechecks cassettes for diffs

Avoids versioning issues

Page 67: Effectively Testing Services - Burlington Ruby Conf

gem 'puffing-billy'

Page 68: Effectively Testing Services - Burlington Ruby Conf

Puffing-Billy Built for in-browser requests

Allowed to record and reuse (like VCR)

Page 69: Effectively Testing Services - Burlington Ruby Conf

Be brave, venture out of ruby

Page 70: Effectively Testing Services - Burlington Ruby Conf

I also like…

Page 71: Effectively Testing Services - Burlington Ruby Conf

Chrome Dev Tools

Page 72: Effectively Testing Services - Burlington Ruby Conf

Postman

Page 73: Effectively Testing Services - Burlington Ruby Conf

HTTPie

Page 74: Effectively Testing Services - Burlington Ruby Conf

Charles

Page 75: Effectively Testing Services - Burlington Ruby Conf

Additional Reading martinfowler.com/bliki/IntegrationContractTest.html

timrogers.uk/2014/07/12/discovering-private-apis-with-charles-app/ robots.thoughtbot.com/how-to-stub-external-services-in-tests

joblivious.wordpress.com/2009/02/20/handling-intermittence-how-to-survive-test-driven-development

railscasts.com/episodes/291-testing-with-vcr

Page 76: Effectively Testing Services - Burlington Ruby Conf

Bringing it all together Testing services is crucial

If in doubt, stub it out

Determine the flexibility you want

Record responses to save time

Page 77: Effectively Testing Services - Burlington Ruby Conf

Next Up How to Consume Lots of Data

with Doug Alcorn

Page 78: Effectively Testing Services - Burlington Ruby Conf

Thank you! [email protected]

[email protected] (I like getting email)

@neal_kemp

(I tweet)