Effectively Testing Services on Rails - Railsconf 2014

76
Effectively Testing Services Neal Kemp

description

Testing services with Ruby on Rails for Railsconf 2014. Test SOA or external services with ease.

Transcript of Effectively Testing Services on Rails - Railsconf 2014

Page 1: Effectively Testing Services on Rails - Railsconf 2014

Effectively

Testing

ServicesNeal Kemp

Page 2: Effectively Testing Services on Rails - Railsconf 2014

$ whoami

Iowa native

Now: Californian

Software Developer

Independent Consultant

Page 3: Effectively Testing Services on Rails - Railsconf 2014

What I Do

Ruby / Rails

Javascript / Angular

HTML, CSS, etc

Page 4: Effectively Testing Services on Rails - Railsconf 2014

what, why & howof testing services

Page 5: Effectively Testing Services on Rails - Railsconf 2014

NOT Building testable services

Page 6: Effectively Testing Services on Rails - Railsconf 2014

NOT Test-driven development

(necessarily)

Page 7: Effectively Testing Services on Rails - Railsconf 2014

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

Page 8: Effectively Testing Services on Rails - Railsconf 2014

what

Page 9: Effectively Testing Services on Rails - Railsconf 2014

What is a service?

Internal “SOA”

Page 10: Effectively Testing Services on Rails - Railsconf 2014

Any time you make an HTTP

request to an endpoint in

another repository

Page 11: Effectively Testing Services on Rails - Railsconf 2014

why

Page 12: Effectively Testing Services on Rails - Railsconf 2014

Why are services important?

Build faster

Makes scaling easier

Use them on virtually every application

Increasingly prevalent

Page 13: Effectively Testing Services on Rails - Railsconf 2014

Services are critical to

modern Rails

development

Page 14: Effectively Testing Services on Rails - Railsconf 2014

Why is testing services

important?You (should) test everything else

Services compose crucial features

You may encounter problems…

Page 15: Effectively Testing Services on Rails - Railsconf 2014

Internal API

Sometimes null responses

Inconsistencies

Catastrophe

Page 16: Effectively Testing Services on Rails - Railsconf 2014

Okay? But what about external APIs?

Page 17: Effectively Testing Services on Rails - Railsconf 2014
Page 18: Effectively Testing Services on Rails - Railsconf 2014

{"id": 24}

{"code": "ANA"}

Page 19: Effectively Testing Services on Rails - Railsconf 2014

"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 20: Effectively Testing Services on Rails - Railsconf 2014

No versioning!

Page 21: Effectively Testing Services on Rails - Railsconf 2014
Page 22: Effectively Testing Services on Rails - Railsconf 2014

Snapchat Client

Haphazard documentation

What are the requests?

Bizarre obfuscation

github.com/nneal/snapcat

Page 23: Effectively Testing Services on Rails - Railsconf 2014

how

Page 24: Effectively Testing Services on Rails - Railsconf 2014

What is different about

services?External network requests

You don’t own the code

Page 25: Effectively Testing Services on Rails - Railsconf 2014
Page 26: Effectively Testing Services on Rails - Railsconf 2014

On an airplane…

Failure is bad!

No network requests

Page 27: Effectively Testing Services on Rails - Railsconf 2014

Don’t interact with services from

test environment* **

Page 28: Effectively Testing Services on Rails - Railsconf 2014

* Includes “dummy” APIs

Page 29: Effectively Testing Services on Rails - Railsconf 2014

** Using pre-recorded

responses is okay

Page 30: Effectively Testing Services on Rails - Railsconf 2014

Assuming:

Rails, rspec

Page 31: Effectively Testing Services on Rails - Railsconf 2014

Time to stub!

Page 32: Effectively Testing Services on Rails - Railsconf 2014

Built-in Stubbing

Typhoeus

Faraday

Excon

Page 33: Effectively Testing Services on Rails - Railsconf 2014

Simplify.

Page 34: Effectively Testing Services on Rails - Railsconf 2014

gem 'webmock'

Page 35: Effectively Testing Services on Rails - Railsconf 2014

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’

end

WebMock.disable_net_connect!

spec/spec_helper.rb

Page 36: Effectively Testing Services on Rails - Railsconf 2014
Page 37: Effectively Testing Services on Rails - Railsconf 2014

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

)

end

end

lib/facebook_wrapper.rb

Page 38: Effectively Testing Services on Rails - Railsconf 2014

require 'facebook_wrapper'

config/intializers/facebook_wrapper.rb

Page 39: Effectively Testing Services on Rails - Railsconf 2014

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'

end

end

spec/lib/facebook_wrapper_spec.rb

Page 40: Effectively Testing Services on Rails - Railsconf 2014

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'

end

end

spec/lib/facebook_wrapper_spec.rb

Page 41: Effectively Testing Services on Rails - Railsconf 2014

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'

end

end

spec/lib/facebook_wrapper_spec.rb

Page 42: Effectively Testing Services on Rails - Railsconf 2014

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'

end

end

spec/lib/facebook_wrapper_spec.rb

Page 43: Effectively Testing Services on Rails - Railsconf 2014

Even Better

No network requests

Fast!

No intermittent failure

Page 44: Effectively Testing Services on Rails - Railsconf 2014

Mock-Services

AWS

FB graph mock

OmniAuth

Etc…

Page 45: Effectively Testing Services on Rails - Railsconf 2014

gem 'fb_graph-mock'

Page 46: Effectively Testing Services on Rails - Railsconf 2014

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::Mock

end

WebMock.disable_net_connect!

spec/spec_helper.rb

Page 47: Effectively Testing Services on Rails - Railsconf 2014

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

end

end

spec/lib/facebook_wrapper_spec.rb

Page 48: Effectively Testing Services on Rails - Railsconf 2014

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

end

end

spec/lib/facebook_wrapper_spec.rb

Page 49: Effectively Testing Services on Rails - Railsconf 2014

Even Better

Already stubbed for you

Pre-recorded responses (sometimes)

Don’t need to know API endpoints

Page 50: Effectively Testing Services on Rails - Railsconf 2014

gem 'sham_rack'

Page 51: Effectively Testing Services on Rails - Railsconf 2014

gem 'sinatra'

Page 52: Effectively Testing Services on Rails - Railsconf 2014

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’

end

WebMock.disable_net_connect!

spec/spec_helper.rb

Page 53: Effectively Testing Services on Rails - Railsconf 2014

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"

}|

end

end

spec/support/fake_facebook.rb

Page 54: Effectively Testing Services on Rails - Railsconf 2014

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"

}|

end

end

spec/support/fake_facebook.rb

Page 55: Effectively Testing Services on Rails - Railsconf 2014

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"

}|

end

end

spec/support/fake_facebook.rb

Page 56: Effectively Testing Services on Rails - Railsconf 2014

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"

}|

end

end

spec/support/fake_facebook.rb

Page 57: Effectively Testing Services on Rails - Railsconf 2014

describe FacebookWrapper, '.user_link' do

it 'retrieves user link' do

user_id = FacebookWrapper.user_id('arjun')

expect(user_id).to eq '7901103’

end

end

spec/lib/facebook_wrapper_spec.rb

Page 58: Effectively Testing Services on Rails - Railsconf 2014

Even Better

Dynamic

Expressive

Readable

Page 59: Effectively Testing Services on Rails - Railsconf 2014

gem 'vcr'

Page 60: Effectively Testing Services on Rails - Railsconf 2014

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’

end

WebMock.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 61: Effectively Testing Services on Rails - Railsconf 2014

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

end

end

spec/lib/facebook_wrapper_spec.rb

Page 62: Effectively Testing Services on Rails - Railsconf 2014

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

end

end

spec/lib/facebook_wrapper_spec.rb

Page 63: Effectively Testing Services on Rails - Railsconf 2014

Even Better

Record API automatically

Replay responses without network

Verify responses

Page 64: Effectively Testing Services on Rails - Railsconf 2014

Additional Build Process

Runs outside normal test mode

Rechecks cassettes for diffs

Avoids versioning issues

Page 65: Effectively Testing Services on Rails - Railsconf 2014

gem 'puffing-billy'

Page 66: Effectively Testing Services on Rails - Railsconf 2014

Puffing-Billy

Built for in-browser requests

Allowed to record and reuse (like VCR)

Page 67: Effectively Testing Services on Rails - Railsconf 2014

Be brave, venture out

of ruby

Page 68: Effectively Testing Services on Rails - Railsconf 2014

I also like…

Page 69: Effectively Testing Services on Rails - Railsconf 2014

Chrome Dev Tools

Page 70: Effectively Testing Services on Rails - Railsconf 2014

Postman

Page 71: Effectively Testing Services on Rails - Railsconf 2014

HTTPie

Page 72: Effectively Testing Services on Rails - Railsconf 2014

Charles

Page 73: Effectively Testing Services on Rails - Railsconf 2014

Additional Readingmartinfowler.com/bliki/IntegrationContractTest.html

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 74: Effectively Testing Services on Rails - Railsconf 2014

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 75: Effectively Testing Services on Rails - Railsconf 2014

Next Up

Eliminating Inconsistent Test Failures

with Austin Putman

Page 76: Effectively Testing Services on Rails - Railsconf 2014

Thank you!

[email protected]

(I like emails)

@neal_kemp

(I tweet)