From cfbea91a69a04c056300de961cc771b10de6c950 Mon Sep 17 00:00:00 2001 From: Yutaka Kamei Date: Thu, 28 Jul 2022 18:03:13 +0900 Subject: [PATCH] Support Proc type for stubbed request body (#1436) Previously, the Faraday testing adapter compared the request body to the stubbed body just by calling `#==` if the stubbed body is present. Sometimes, I want to check the equality between request and stubbed body in more advanced ways. For example, there is a case that I want to check only the parts of the body are actually passed to Faraday instance like this: ```ruby stubs = Faraday::Adapter::Test::Stubs.new do |stub| stub.post('/foo', '{"name:"YK","created_at":"ANY STRING IS OK"}') { [200, {}, ''] } end connection.post('/foo', JSON.dump(name: 'YK', created_at: Time.now)) stubs.verify_stubbed_calls ``` In this case, it's difficult to make tests always pass with `"created_at"` because the value is dynamic. So, I came up with an idea to pass a proc as a stubbed value and compare bodies, inside the proc: ```ruby stubs = Faraday::Adapter::Test::Stubs.new do |stub| check = -> (request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } stub.post('/foo', check) { [200, {}, ''] } end connection.post('/foo', JSON.dump(name: 'YK', created_at: Time.now)) stubs.verify_stubbed_calls ``` I believe this would be flexible but compatible with the previous behavior. --- docs/adapters/testing.md | 8 +++++++ examples/client_spec.rb | 22 +++++++++++++++++++ examples/client_test.rb | 26 ++++++++++++++++++++++ lib/faraday/adapter/test.rb | 28 +++++++++++++++++++++++- spec/faraday/adapter/test_spec.rb | 36 +++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 1 deletion(-) diff --git a/docs/adapters/testing.md b/docs/adapters/testing.md index 4d28c395..8031bd3c 100644 --- a/docs/adapters/testing.md +++ b/docs/adapters/testing.md @@ -64,6 +64,14 @@ initialized. This is useful for testing. stubs.get('/uni') { |env| [ 200, {}, 'urchin' ]} ``` +You can also stub the request body with a string or a proc. +It would be useful to pass a proc if it's OK only to check the parts of the request body are passed. + +```ruby +stubs.post('/kohada', 'where=sea&temperature=24') { |env| [ 200, {}, 'spotted gizzard shad' ]} +stubs.post('/anago', -> (request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'Wakamoto' } }) { |env| [200, {}, 'conger eel'] } +``` + If you want to stub requests that exactly match a path, parameters, and headers, `strict_mode` would be useful. diff --git a/examples/client_spec.rb b/examples/client_spec.rb index 68fabf8f..e30d86f7 100644 --- a/examples/client_spec.rb +++ b/examples/client_spec.rb @@ -17,6 +17,11 @@ class Client data = JSON.parse(res.body) data['origin'] end + + def foo(params) + res = @conn.post('/foo', JSON.dump(params)) + res.status + end end RSpec.describe Client do @@ -94,4 +99,21 @@ RSpec.describe Client do stubs.verify_stubbed_calls end end + + context 'When you want to test the body, you can use a proc as well as string' do + it 'tests with a string' do + stubs.post('/foo', '{"name":"YK"}') { [200, {}, ''] } + + expect(client.foo(name: 'YK')).to eq 200 + stubs.verify_stubbed_calls + end + + it 'tests with a proc' do + check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } + stubs.post('/foo', check) { [200, {}, ''] } + + expect(client.foo(name: 'YK', created_at: Time.now)).to eq 200 + stubs.verify_stubbed_calls + end + end end diff --git a/examples/client_test.rb b/examples/client_test.rb index 58b3165d..3aad9576 100644 --- a/examples/client_test.rb +++ b/examples/client_test.rb @@ -18,6 +18,11 @@ class Client data = JSON.parse(res.body) data['origin'] end + + def foo(params) + res = @conn.post('/foo', JSON.dump(params)) + res.status + end end # Example API client test @@ -109,6 +114,27 @@ class ClientTest < Test::Unit::TestCase stubs.verify_stubbed_calls end + def test_with_string_body + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + stub.post('/foo', '{"name":"YK"}') { [200, {}, ''] } + end + cli = client(stubs) + assert_equal 200, cli.foo(name: 'YK') + + stubs.verify_stubbed_calls + end + + def test_with_proc_body + stubs = Faraday::Adapter::Test::Stubs.new do |stub| + check = ->(request_body) { JSON.parse(request_body).slice('name') == { 'name' => 'YK' } } + stub.post('/foo', check) { [200, {}, ''] } + end + cli = client(stubs) + assert_equal 200, cli.foo(name: 'YK', created_at: Time.now) + + stubs.verify_stubbed_calls + end + def client(stubs) conn = Faraday.new do |builder| builder.adapter :test, stubs diff --git a/lib/faraday/adapter/test.rb b/lib/faraday/adapter/test.rb index cb07ca60..9e539e61 100644 --- a/lib/faraday/adapter/test.rb +++ b/lib/faraday/adapter/test.rb @@ -26,6 +26,15 @@ module Faraday # ] # end # + # # Test the request body is the same as the stubbed body + # stub.post('/bar', 'name=YK&word=call') { [200, {}, ''] } + # + # # You can pass a proc as a stubbed body and check the request body in your way. + # # In this case, the proc should return true or false. + # stub.post('/foo', ->(request_body) do + # JSON.parse(request_body).slice('name') == { 'name' => 'YK' } }) { [200, {}, ''] + # end + # # # You can set strict_mode to exactly match the stubbed requests. # stub.strict_mode = true # end @@ -42,6 +51,12 @@ module Faraday # # resp = test.get '/items/2' # resp.body # => 'showing item: 2' + # + # resp = test.post '/bar', 'name=YK&word=call' + # resp.status # => 200 + # + # resp = test.post '/foo', JSON.dump(name: 'YK', created_at: Time.now) + # resp.status # => 200 class Test < Faraday::Adapter attr_accessor :stubs @@ -181,7 +196,7 @@ module Faraday [(host.nil? || host == request_host) && path_match?(request_path, meta) && params_match?(env) && - (body.to_s.size.zero? || request_body == body) && + body_match?(request_body) && headers_match?(request_headers), meta] end @@ -222,6 +237,17 @@ module Faraday end end + def body_match?(request_body) + return true if body.to_s.size.zero? + + case body + when Proc + body.call(request_body) + else + request_body == body + end + end + def to_s "#{path} #{body}" end diff --git a/spec/faraday/adapter/test_spec.rb b/spec/faraday/adapter/test_spec.rb index 44759173..bdeb6cdf 100644 --- a/spec/faraday/adapter/test_spec.rb +++ b/spec/faraday/adapter/test_spec.rb @@ -373,5 +373,41 @@ RSpec.describe Faraday::Adapter::Test do it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent' } it_behaves_like 'does not raise NotFound even when headers do not satisfy the strict check', '/with_user_agent', { authorization: 'Bearer m_ck', user_agent: 'My Agent', x_special: 'special' } end + + describe 'body_match?' do + let(:stubs) do + described_class::Stubs.new do |stubs| + stubs.post('/no_check') { [200, {}, 'ok'] } + stubs.post('/with_string', 'abc') { [200, {}, 'ok'] } + stubs.post( + '/with_proc', + ->(request_body) { JSON.parse(request_body, symbolize_names: true) == { x: '!', a: [{ m: [{ a: true }], n: 123 }] } }, + { content_type: 'application/json' } + ) do + [200, {}, 'ok'] + end + end + end + + context 'when trying without any args for body' do + subject(:without_body) { connection.post('/no_check') } + + it { expect(without_body.status).to eq 200 } + end + + context 'when trying with string body stubs' do + subject(:with_string) { connection.post('/with_string', 'abc') } + + it { expect(with_string.status).to eq 200 } + end + + context 'when trying with proc body stubs' do + subject(:with_proc) do + connection.post('/with_proc', JSON.dump(a: [{ n: 123, m: [{ a: true }] }], x: '!'), { 'Content-Type' => 'application/json' }) + end + + it { expect(with_proc.status).to eq 200 } + end + end end end