├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── lib ├── savon │ ├── spec.rb │ └── spec │ │ ├── fixture.rb │ │ ├── macros.rb │ │ ├── mock.rb │ │ └── version.rb └── savon_spec.rb ├── savon_spec.gemspec └── spec ├── fixtures └── get_user │ └── success.xml ├── savon └── spec │ ├── fixture_spec.rb │ ├── macros_spec.rb │ └── mock_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .yardoc 3 | doc 4 | coverage 5 | tmp 6 | *~ 7 | *.gem 8 | .bundle 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/travis-ci/travis-ci/wiki/.travis.yml-options 2 | language: "ruby" 3 | script: "bundle exec rake" 4 | rvm: 5 | - 1.8.7 6 | - 1.9.2 7 | - 1.9.3 8 | - jruby-18mode 9 | - jruby-19mode 10 | - rbx-18mode 11 | - rbx-19mode 12 | - ree 13 | notifications: 14 | irc: "irc.freenode.org#savon" 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Daniel Harrington 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Savon::Spec [![Build Status](https://secure.travis-ci.org/rubiii/savon_spec.png)](http://travis-ci.org/rubiii/savon_spec) 2 | =========== 3 | 4 | Savon v1 Rspec Helpers 5 | 6 | Deprecated 7 | ========== 8 | 9 | Starting in Savon v2, [spec helpers are included in Savon itself](http://savonrb.com/version2/testing.html). This gem is only helpful if you're using Savon v1. It is highly recommended to use Savon v2 with the `Savon::SpecHelper` module for new projects. 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | Savon::Spec is available through [Rubygems](http://rubygems.org/gems/savon_spec) and can be installed via: 16 | 17 | ``` 18 | $ gem install savon_spec 19 | ``` 20 | 21 | 22 | Expects 23 | ------- 24 | 25 | Include the `Savon::Spec::Macros` module into your specs: 26 | 27 | ``` ruby 28 | RSpec.configure do |config| 29 | config.include Savon::Spec::Macros 30 | end 31 | ``` 32 | 33 | By including the module you get a `savon` method to mock SOAP requests. Here's a very simple example: 34 | 35 | ``` ruby 36 | let(:client) do 37 | Savon::Client.new do 38 | wsdl.endpoint = "http://example.com" 39 | wsdl.namespace = "http://users.example.com" 40 | end 41 | end 42 | 43 | before do 44 | savon.expects(:get_user) 45 | end 46 | 47 | it "mocks a SOAP request" do 48 | client.request(:get_user) 49 | end 50 | ``` 51 | 52 | This sets up an expectation for Savon to call the `:get_user` action and the specs should pass without errors. 53 | Savon::Spec does not execute a POST request to your service, but uses [Savon hooks](http://savonrb.com/#hook_into_the_system) to return a fake response: 54 | 55 | ``` ruby 56 | { :code => 200, :headers => {}, :body => "" } 57 | ``` 58 | 59 | To further isolate your specs, I'd suggest setting up [FakeWeb](http://rubygems.org/gems/fakeweb) to disallow any HTTP requests. 60 | 61 | 62 | With 63 | ---- 64 | 65 | Mocking SOAP requests is fine, but what you really need to do is verify whether you're sending the right 66 | parameters to your service. 67 | 68 | ``` ruby 69 | before do 70 | savon.expects(:get_user).with(:id => 1) 71 | end 72 | 73 | it "mocks a SOAP request" do 74 | client.request(:get_user) do 75 | soap.body = { :id => 1 } 76 | end 77 | end 78 | ``` 79 | 80 | This checks whether Savon uses the SOAP body Hash you expected and raises a `Savon::Spec::ExpectationError` if it doesn't. 81 | 82 | ``` 83 | Failure/Error: client.request :get_user, :body => { :name => "Dr. Who" } 84 | Savon::Spec::ExpectationError: 85 | expected { :id => 1 } to be sent, got: { :name => "Dr. Who" } 86 | ``` 87 | 88 | You can also pass a block to the `#with` method and receive the `Savon::SOAP::Request` before the POST request is executed. 89 | Here's an example of a custom expectation: 90 | 91 | ``` ruby 92 | savon.expects(:get_user).with do |request| 93 | request.soap.body.should include(:id) 94 | end 95 | ``` 96 | 97 | 98 | Returns 99 | ------- 100 | 101 | Instead of the default fake response, you can return a custom HTTP response by passing a Hash to the `#returns` method. 102 | If you leave out any of these values, Savon::Spec will add the default values for you. 103 | 104 | ``` ruby 105 | savon.expects(:get_user).returns(:code => 500, :headers => {}, :body => "save the unicorns") 106 | ``` 107 | 108 | Savon::Spec also works with SOAP response fixtures (simple XML files) and a conventional folder structure: 109 | 110 | ``` 111 | ~ spec 112 | ~ fixtures 113 | ~ get_user 114 | - single_user.xml 115 | - multiple_users.xml 116 | + models 117 | + controllers 118 | + helpers 119 | + views 120 | ``` 121 | 122 | When used inside a Rails 3 application, Savon::Spec uses `Rails.root.join("spec", "fixtures")` to locate your fixture directory. 123 | In any other case, you have to manually set the fixture path via: 124 | 125 | ``` ruby 126 | Savon::Spec::Fixture.path = File.expand_path("../fixtures", __FILE__) 127 | ``` 128 | 129 | Directory names inside the fixtures directory map to SOAP actions and contain actual SOAP responses from your service(s). 130 | You can use one of those fixtures for the HTTP response body like in the following example: 131 | 132 | ``` ruby 133 | savon.expects(:get_user).with(:id => 1).returns(:single_user) 134 | ``` 135 | 136 | As you can see, Savon::Spec uses the name of your SOAP action and the Symbol passed to the `#returns` method to navigate inside 137 | your fixtures directory and load the requested XML files. 138 | 139 | 140 | Never 141 | ----- 142 | 143 | Savon::Spec can also verify that a certain SOAP request was not executed: 144 | 145 | ``` ruby 146 | savon.expects(:get_user).never 147 | ``` 148 | 149 | 150 | RSpec 151 | ----- 152 | 153 | This library is optimized to work with RSpec, but it could be tweaked to work with any other testing library. 154 | Savon::Spec installs an after filter to clear out its Savon hooks after each example. 155 | 156 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec 8 | 9 | -------------------------------------------------------------------------------- /lib/savon/spec.rb: -------------------------------------------------------------------------------- 1 | require "savon" 2 | require "rspec" 3 | 4 | module Savon 5 | module Spec 6 | 7 | autoload :Macros, "savon/spec/macros" 8 | autoload :Mock, "savon/spec/mock" 9 | autoload :Fixture, "savon/spec/fixture" 10 | 11 | end 12 | end 13 | 14 | RSpec.configure do |config| 15 | config.after { Savon.config.hooks.reject(Savon::Spec::Mock::HOOKS) } 16 | end 17 | -------------------------------------------------------------------------------- /lib/savon/spec/fixture.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | module Spec 3 | 4 | # = Savon::Spec::Fixture 5 | # 6 | # Manages SOAP response fixtures. 7 | class Fixture 8 | class << self 9 | 10 | def path 11 | @path ||= Rails.root.join("spec", "fixtures").to_s if defined? Rails 12 | 13 | raise ArgumentError, "Savon::Spec::Fixture.path needs to be specified" unless @path 14 | @path 15 | end 16 | 17 | attr_writer :path 18 | 19 | def load(*args) 20 | file = args.map { |arg| arg.to_s.snakecase }.join("/") 21 | fixtures[file] ||= read_file(file) 22 | end 23 | 24 | alias [] load 25 | 26 | private 27 | 28 | def fixtures 29 | @fixtures ||= {} 30 | end 31 | 32 | def read_file(file) 33 | full_path = File.expand_path File.join(path, "#{file}.xml") 34 | raise ArgumentError, "Unable to load: #{full_path}" unless File.exist? full_path 35 | 36 | File.read full_path 37 | end 38 | 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/savon/spec/macros.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | module Spec 3 | 4 | # = Savon::Spec::Macros 5 | # 6 | # Include this module into your RSpec tests to mock Savon SOAP requests. 7 | module Macros 8 | 9 | def savon 10 | Mock.new 11 | end 12 | 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/savon/spec/mock.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | module Spec 3 | 4 | class ExpectationError < RuntimeError; end 5 | 6 | # = Savon::Spec::Mock 7 | # 8 | # Mocks Savon SOAP requests. 9 | class Mock 10 | 11 | # Hooks registered by Savon::Spec. 12 | HOOKS = [:spec_action, :spec_body, :spec_response, :spec_never] 13 | 14 | # Expects that a given +action+ should be called. 15 | def expects(expected) 16 | self.action = expected 17 | 18 | Savon.config.hooks.define(:spec_action, :soap_request) do |_, request| 19 | actual = request.soap.input[1] 20 | raise ExpectationError, "expected #{action.inspect} to be called, got: #{actual.inspect}" unless actual == action 21 | 22 | respond_with 23 | end 24 | 25 | self 26 | end 27 | 28 | # Accepts a SOAP +body+ to check if it was set. Also accepts a +block+ 29 | # which receives the Savon::SOAP::Request to set up custom expectations. 30 | def with(body = nil, &block) 31 | Savon.config.hooks.define(:spec_body, :soap_request) do |_, request| 32 | if block 33 | block.call(request) 34 | else 35 | actual = request.soap.body 36 | raise ExpectationError, "expected #{body.inspect} to be sent, got: #{actual.inspect}" unless actual == body 37 | end 38 | 39 | respond_with 40 | end 41 | 42 | self 43 | end 44 | 45 | # Expects a given +response+ to be returned. 46 | def returns(response = nil) 47 | http = case response 48 | when Symbol then { :body => Fixture[action, response] } 49 | when Hash then response 50 | end 51 | 52 | Savon.config.hooks.define(:spec_response, :soap_request) do |_, request| 53 | respond_with(http) 54 | end 55 | 56 | self 57 | end 58 | 59 | # Expects that the +action+ doesn't get called. 60 | def never 61 | Savon.config.hooks.reject(:spec_action) 62 | 63 | Savon.config.hooks.define(:spec_never, :soap_request) do |_, request| 64 | actual = request.soap.input[1] 65 | raise ExpectationError, "expected #{action.inspect} never to be called, but it was!" if actual == action 66 | 67 | respond_with 68 | end 69 | 70 | self 71 | end 72 | 73 | private 74 | 75 | def action=(action) 76 | @action = action.is_a?(String) ? action.to_sym : lower_camelcase(action.to_s).to_sym 77 | end 78 | 79 | attr_reader :action 80 | 81 | def respond_with(http = {}) 82 | defaults = { :code => 200, :headers => {}, :body => "" } 83 | http = defaults.merge(http) 84 | 85 | HTTPI::Response.new(http[:code], http[:headers], http[:body]) 86 | end 87 | 88 | # Extracted from CoreExt of Savon 0.9.9 89 | def lower_camelcase(source_str) 90 | str = source_str.dup 91 | str.gsub!(/\/(.?)/) { "::#{$1.upcase}" } 92 | str.gsub!(/(?:_+|-+)([a-z])/) { $1.upcase } 93 | str.gsub!(/(\A|\s)([A-Z])/) { $1 + $2.downcase } 94 | str 95 | end 96 | 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /lib/savon/spec/version.rb: -------------------------------------------------------------------------------- 1 | module Savon 2 | module Spec 3 | 4 | VERSION = "1.3.0" 5 | 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/savon_spec.rb: -------------------------------------------------------------------------------- 1 | require "savon/spec" 2 | require "savon/spec/version" 3 | -------------------------------------------------------------------------------- /savon_spec.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "savon/spec/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "savon_spec" 7 | s.version = Savon::Spec::VERSION 8 | s.authors = "Daniel Harrington" 9 | s.email = "me@rubiii.com" 10 | s.homepage = "http://github.com/rubiii/#{s.name}" 11 | s.summary = "Savon testing library" 12 | s.description = s.summary 13 | 14 | s.rubyforge_project = s.name 15 | 16 | s.add_dependency "savon", "~> 1.0" 17 | s.add_dependency "rspec", ">= 2.0.0" 18 | 19 | s.add_development_dependency "httpclient", "~> 2.1.5" 20 | s.add_development_dependency "webmock", "~> 1.4.0" 21 | 22 | s.add_development_dependency "autotest" 23 | s.add_development_dependency "rake" 24 | s.add_development_dependency "ZenTest", "4.5.0" 25 | 26 | s.files = `git ls-files`.split("\n") 27 | s.require_path = "lib" 28 | end 29 | -------------------------------------------------------------------------------- /spec/fixtures/get_user/success.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dr. Who 6 | dr_who@example.com 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/savon/spec/fixture_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::Spec::Fixture do 4 | 5 | describe ".path" do 6 | 7 | it "returns a specified path" do 8 | Savon::Spec::Fixture.path = "/Users/dr_who/some_app/spec/fixtures" 9 | Savon::Spec::Fixture.path.should == "/Users/dr_who/some_app/spec/fixtures" 10 | 11 | Savon::Spec::Fixture.path = nil # reset to default 12 | end 13 | 14 | it "raises an ArgumentError when accessed before specified" do 15 | expect { Savon::Spec::Fixture.path }.to raise_error(ArgumentError) 16 | end 17 | 18 | it "defaults to spec/fixtures when used in a Rails app" do 19 | Rails = Class.new do 20 | def self.root 21 | Pathname.new("/Users/dr_who/another_app") 22 | end 23 | end 24 | 25 | Savon::Spec::Fixture.path.should == "/Users/dr_who/another_app/spec/fixtures" 26 | 27 | Object.send(:remove_const, "Rails") 28 | end 29 | 30 | end 31 | 32 | describe ".load" do 33 | 34 | around do |example| 35 | Savon::Spec::Fixture.path = "spec/fixtures" 36 | example.run 37 | Savon::Spec::Fixture.path = nil # reset to default 38 | end 39 | 40 | it "returns a fixture for the given arguments" do 41 | fixture = Savon::Spec::Fixture.load :get_user, :success 42 | fixture.should == File.read("spec/fixtures/get_user/success.xml") 43 | end 44 | 45 | it "memoizes the fixtures" do 46 | Savon::Spec::Fixture.load(:get_user, :success). 47 | should equal(Savon::Spec::Fixture.load(:get_user, :success)) 48 | end 49 | 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /spec/savon/spec/macros_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::Spec::Macros do 4 | include Savon::Spec::Macros 5 | 6 | describe "#savon" do 7 | it "returns a Savon::Spec::Mock instance" do 8 | savon.should be_a(Savon::Spec::Mock) 9 | end 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/savon/spec/mock_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Savon::Spec::Mock do 4 | include Savon::Spec::Macros 5 | 6 | let(:client) do 7 | Savon::Client.new do 8 | wsdl.endpoint = "http://example.com" 9 | wsdl.namespace = "http://users.example.com" 10 | end 11 | end 12 | 13 | describe "#expects" do 14 | 15 | before do 16 | savon.expects(:get_user) 17 | end 18 | 19 | it "does not execute a POST request (verified via WebMock)" do 20 | client.request(:get_user) 21 | end 22 | 23 | it "fails when a different SOAP action was called" do 24 | expect { client.request(:get_user_by_id) }.to raise_error( 25 | Savon::Spec::ExpectationError, 26 | "expected :getUser to be called, got: :getUserById" 27 | ) 28 | end 29 | 30 | it "should not lower_camelcase actions that are passed as string" do 31 | expect { client.request("GetUser") }.to raise_error( 32 | Savon::Spec::ExpectationError, 33 | "expected :getUser to be called, got: :GetUser" 34 | ) 35 | end 36 | end 37 | 38 | describe "#with" do 39 | 40 | context "a Hash" do 41 | 42 | before do 43 | savon.expects(:get_user).with(:id => 1) 44 | end 45 | 46 | it "expects Savon to send a specific SOAP body" do 47 | client.request :get_user, :body => { :id => 1 } 48 | end 49 | 50 | it "fails when the SOAP body was not set" do 51 | expect { client.request(:get_user) }.to raise_error( 52 | Savon::Spec::ExpectationError, 53 | "expected {:id=>1} to be sent, got: nil" 54 | ) 55 | end 56 | 57 | it "fails when the SOAP body did not match the expected value" do 58 | expect { client.request :get_user, :body => { :id => 2 } }.to raise_error( 59 | Savon::Spec::ExpectationError, 60 | "expected {:id=>1} to be sent, got: {:id=>2}" 61 | ) 62 | end 63 | 64 | end 65 | 66 | context "a block" do 67 | 68 | before do 69 | savon.expects(:get_user).with do |request| 70 | request.soap.body.should include(:id) 71 | end 72 | end 73 | 74 | it "works with a custom expectation" do 75 | client.request :get_user, :body => { :id => 1 } 76 | end 77 | 78 | it "fails when the expectation was not met" do 79 | begin 80 | client.request :get_user, :body => { :name => "Dr. Who" } 81 | rescue Spec::Expectations::ExpectationNotMetError => e 82 | e.message.should =~ /expected \{:name=>"Dr. Who"\} to include :id/ 83 | end 84 | end 85 | 86 | end 87 | 88 | end 89 | 90 | describe "#never" do 91 | 92 | before do 93 | savon.expects(:noop).never 94 | end 95 | 96 | it "expects Savon to never call a specific SOAP action" do 97 | expect { client.request(:noop) }.to raise_error( 98 | Savon::Spec::ExpectationError, 99 | "expected :noop never to be called, but it was!" 100 | ) 101 | end 102 | 103 | end 104 | 105 | describe "#returns" do 106 | 107 | context "without arguments" do 108 | 109 | let(:response) do 110 | client.request(:get_user) 111 | end 112 | 113 | before do 114 | savon.expects(:get_user) 115 | end 116 | 117 | it "returns a response code of 200" do 118 | response.http.code.should == 200 119 | end 120 | 121 | it "does not return any response headers" do 122 | response.http.headers.should == {} 123 | end 124 | 125 | it "returns an empty response body" do 126 | response.http.body.should == "" 127 | end 128 | 129 | end 130 | 131 | context "with a Symbol" do 132 | 133 | let(:response) do 134 | client.request(:get_user) 135 | end 136 | 137 | around do |example| 138 | Savon::Spec::Fixture.path = "spec/fixtures" 139 | savon.expects(:get_user).returns(:success) 140 | 141 | example.run 142 | 143 | Savon::Spec::Fixture.path = nil # reset to default 144 | end 145 | 146 | it "returns a response code of 200" do 147 | response.http.code.should == 200 148 | end 149 | 150 | it "does not return any response headers" do 151 | response.http.headers.should == {} 152 | end 153 | 154 | it "returns the :success fixture for the :get_user action" do 155 | response.http.body.should == File.read("spec/fixtures/get_user/success.xml") 156 | end 157 | 158 | end 159 | 160 | context "with a Hash" do 161 | 162 | let(:response) do 163 | client.request(:get_user) 164 | end 165 | 166 | let(:http) do 167 | { :code => 201, :headers => { "Set-Cookie" => "ID=1; Max-Age=3600;" }, :body => "cookie" } 168 | end 169 | 170 | before do 171 | savon.expects(:get_user).returns(http) 172 | end 173 | 174 | it "returns the given response code" do 175 | response.http.code.should == http[:code] 176 | end 177 | 178 | it "returns the given response headers" do 179 | response.http.headers.should == http[:headers] 180 | end 181 | 182 | it "returns the given response body" do 183 | response.http.body.should == http[:body] 184 | end 185 | 186 | end 187 | 188 | end 189 | 190 | end 191 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler" 2 | Bundler.require(:default, :development) 3 | 4 | require "savon_spec" 5 | --------------------------------------------------------------------------------