├── .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 [](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 |
--------------------------------------------------------------------------------