How to mock a method call in Rspec - ruby

I'm fairly new to TDD and Rspec. I'm trying to figure out how to make sure a method is being called in test:
module Authentication
include WebRequest
def refresh_auth_token(refresh_token)
"refreshing token"
end
end
class YouTube
include Authentication
attr_accessor :uid, :token, :refresh
def initialize(uid, token, refresh)
#uid = uid
#token = token
#refresh = refresh
# if token has expired, get new token
if #token == nil and #refresh
#token = refresh_auth_token #refresh
end
end
end
And here is my test:
$f = YAML.load_file("fixtures.yaml")
describe YouTube do
data = $f["YouTube"]
subject { YouTube.new(data["uid"], data["token"], data["refresh"]) }
its(:token) { should == data["token"] }
context "when token is nil" do
subject(:without_token) { YouTube.new(data["uid"], nil, data["refresh"]) }
its(:token) { should_not be_nil }
it { YouTube.should_receive(:refresh_auth_token).with(data["refresh"]) }
end
end
But its failing with:
) YouTube when token is nil
Failure/Error: it { YouTube.should_receive(:refresh_auth_token).with(data["refresh"]) }
().refresh_auth_token("1/HBTNQ93otm1cSQH8kKauij3jO0kZQYfgH5J-hBtAP8k")
expected: 1 time with arguments: ("1/HBTNQ93otm1cSQH8kKauij3jO0kZQYfgH5J-hBtAP8k")
received: 0 times with arguments: ("1/HBTNQ93otm1cSQH8kKauij3jO0kZQYfgH5J-hBtAP8k")
# ./lib/youtube/you_tube_test.rb:14:in `block (3 levels) in '
What I'm trying to do in this test, is to determine, when #token is nil, and there is a #refresh supplied, if refresh_auth_token is called on initialize. This mocks and stubs thing is a bit confusing.

Firstly, you want to use any_instance:
YouTube.any_instance.should_receive(:refresh_auth_token).with(data["refresh"])
Currently, you are checking if the class method refresh_auth_token is being called. It isn't, as it doesn't exist.
Next, as the code is executed in the constructor, that line won't catch the call, as the object is already created in the subject line before the spec.
This is the easiest solution:
context "when token is nil" do
it "refreshed the authentation token" do
YouTube.any_instance.should_receive(:refresh_auth_token).with(data["refresh"])
YouTube.new(data["uid"], nil, data["refresh"])
end
end

Related

RSpec double/mock instance variable from initializer

I've got a class where in initializer I need to call instance variable from parsed params:
class PrintResults
include SortResults
attr_accessor :views_hash
def initialize(parser)
#parser = parser
#views_hash = parser.page_views
end
I want to test attributes accessors, I tried something below:
RSpec.describe PrintResults do
subject { described_class.new(views_hash) }
describe 'attributes accessors' do
let(:accessors) { double(page_views: { '/that_70s_show' => ['111.111.111.111'] }) }
it 'should have views hash' do
subject.views_hash = accessors
expect(subject.views_hash).to eq(['111.111.111.111'])
end
end
but I'm getting an error:
1) PrintResults attributes accessors should have views hash
Failure/Error: expect(subject.views_hash).to eq(['111.111.111.111'])
expected: ["111.111.111.111"]
got: #<Double (anonymous)>
(compared using ==)
Diff:
## -1 +1 ##
-["111.111.111.111"]
+#<Double (anonymous)>
You assign your test double directly to the attribute that is returned instead of using the initialize method.
Instead of
subject { described_class.new(views_hash) }
describe 'attributes accessors' do
let(:accessors) { double(page_views: { '/that_70s_show' => ['111.111.111.111'] }) }
it 'should have views hash' do
subject.views_hash = accessors
expect(subject.views_hash).to eq(['111.111.111.111'])
end
end
use
subject { described_class.new(parser) }
describe 'attributes accessors' do
let(:parser) { double(page_views: { '/that_70s_show' => ['111.111.111.111'] }) }
it 'should have views hash' do
expect(subject.views_hash).to eq('/that_70s_show' => ['111.111.111.111'])
end
end

Faraday::RackBuilder methods

in order to use custom middlewares from faraday docs I see that I have to use the use method. In my use case my custom builder just add a jwt auth token in the header:
Faraday.new(url: wsconfig.base_url) do |builder|
builder.use CustomMiddlewares::JwtAuthentication
builder.request :url_encoded
builder.response :json
builder.adapter :net_http
end
jwt_authentication.rb
require 'jwt'
module CustomMiddlewares
class JwtAuthentication < Faraday::Middleware
def call(env)
payload = RequestStore.store[:jwt_claims].to_h.merge({method: env.method, path: env.url.request_uri})
token = jwt(payload)
Rails.logger.debug { " with token: #{token}" }
env[:request_headers]["Authorization"] = "Token: #{token}"
#app.call(env)
rescue StandardError => e
raise "problem in JwtAuthentication Middleware"
end
private
def jwt(payload, expiration = 1.minute.from_now)
payload = payload.dup
payload['exp'] = expiration.to_i
payload['iss'] = 'cgp'
JWT.encode(payload, key, 'RS256')
end
def key
OpenSSL::PKey::RSA.new(Rails.configuration.x.secrets.ws_config.jwt_private_key)
end
end
end
CustomMiddlewares::JwtAuthentication should only be used on request phase like url_encoded middleware which is adde by request method. I wonder why I cannot do the same with mine:
builder.request CustomMiddlewares::JwtAuthentication
I got:
CustomMiddlewares::VerbosingPseudonymizationWs is not registered on Faraday::Request (Faraday::Error)
If you want to use builder.request you first need to register the middleware like this:
Faraday::Request.register_middleware jwt: -> { CustomMiddlewares::JwtAuthentication }
Afterwards you should be able to call builder.request :jwt. This is due to Faraday::RackBuilder#request essentially calling Faraday::RackBuilder#use with Faraday::Request.lookup_middleware(key) as the first parameter.
See https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L92
and https://github.com/lostisland/faraday/blob/master/lib/faraday/rack_builder.rb#L228
This also means that there is no difference between builder.request :jwt and builder.use CustomMiddlewares::JwtAuthentication.
The difference between a request and a response middleware is that the response middlewares should inherit from Faraday::Response::Middleware which makes sure that they only execute on the response (on_complete). See https://github.com/lostisland/faraday/blob/master/lib/faraday/response.rb#L8
I.e., even when registering a middleware via builder.request it can still act on the response if it implements the on_complete callback. Conversely you do not execute any code for the response if you do not implement the callback.

Configure expect in rspec

I want to implement rspec with expect. I tried this:
RSpec:
describe WechatRequestBuilder do
let(:request_builder) { described_class.new(env: 'test_env') }
let(:trx_types) { ['wechat'] }
let(:trx_type) { 'wechat' }
let(:gateway) { 'wechat' }
let(:currency) { 'CNY' }
let(:base_params) { request_builder.send(:base_params) }
it_behaves_like 'request builder', true
context '#submit!' do
it "sends test transactions" do
allow(request_builder).to receive(:process_trx).with(trx_types, gateway)
binding.pry
request_builder.submit!
expect(request_builder.submit!).to receive(:process_trx).with(trx_types, gateway)
end
end
end
Request modifier:
class RequestModifier
def get_trx_type(request_body)
doc = Nokogiri::XML(request_body)
doc.search("transaction_type").first.text
end
end
I tried to find some object with binding.pry but without a luck:
[1] pry(#<RSpec::ExampleGroups::WechatRequestBuilder::Submit>)> request_builder
=> #<WechatRequestBuilder:0x007ffc1af4fd80 #env="test_env", #request_modifier=#<RequestModifier:0x007ffc1af4fd30>>
Can you give e some example based on the above code what should I configure as 'expect'? Currently I get:
(nil).process_trx(["wechat"], "wechat")
expected: 1 time with arguments: (["wechat"], "wechat")
received: 0 times

How to properly mock objects in RSpec?

I have a simple class, which generates a download URL to a file stored on S3 and I need to write a unit test to test this class. So far I've had no luck.
class S3DownloadUrlGenerator
def initialize(filename)
#filename = filename
end
def presigned_url
signer = Aws::S3::Presigner.new(client: s3)
signer.presigned_url(
:get_object,
bucket: "my-bucket",
key: filename,
response_content_disposition: "attachment",
)
end
private
def s3
#s3 ||= Aws::S3::Client.new(
region: "my-region,
http_open_timeout: 5,
http_read_timeout: 25,
)
end
attr_reader :filename
end
I want to test if calling #presigned_url on an instance of S3DownloadUrlGenerator returns a URL.
This is my test:
describe S3DownloadUrlGenerator do
before do
allow(Aws::S3::Client).to receive(:new) { s3_client }
end
let(:s3_client) { spy("s3 client") }
let(:presigner) { spy("s3 presigner") }
it "generates download URL for a file" do
expect(Aws::S3::Presigner).to receive(:new).with(client: s3_client).and_return(presigner)
expect(presigner).to receive(:presigned_url).with(
:get_object,
bucket: "my-test-bucket",
key: "test_file.txt",
response_content_disposition: "attachment",
).and_return("https://www.example.com")
expect(described_class.new("Test_file.txt").presigned_url).to eq("https://www.example.com")
end
end
but I get an error:
Failure/Error: expect(described_class.new("Test_file.txt").presigned_url).to eq("https://www.example.com")
expected: "https://www.example.com"
got: #<Double "s3 presigner">
(compared using ==)
I am bit new to this and I would like to learn how to properly test such cases. Thank you very much for the help.
bucket and key parameters differ in actual calling and mocking.
Use below code it works:
describe S3DownloadUrlGenerator do
before do
allow(Aws::S3::Client).to receive(:new) { s3_client }
end
let(:s3_client) { spy("s3 client") }
let(:presigner) { spy("s3 presigner") }
it "generates download URL for a file" do
expect(Aws::S3::Presigner).to receive(:new).with(client: s3_client).and_return(presigner)
expect(presigner).to receive(:presigned_url).with(
:get_object,
bucket: "my-bucket",
key: "Test_file.txt",
response_content_disposition: "attachment",
).and_return("https://www.example.com")
expect(described_class.new("Test_file.txt").presigned_url).to eq("https://www.example.com")
end
end

Ruby stubbing with faraday, can't get it to work

Sorry for the title, I'm too frustrated to come up with anything better right now.
I have a class, Judge, which has a method #stats. This stats method is supposed to send a GET request to an api and get some data as response. I'm trying to test this and stub the stats method so that I don't perform an actual request. This is what my test looks like:
describe Judge do
describe '.stats' do
context 'when success' do
subject { Judge.stats }
it 'returns stats' do
allow(Faraday).to receive(:get).and_return('some data')
expect(subject.status).to eq 200
expect(subject).to be_success
end
end
end
end
This is the class I'm testing:
class Judge
def self.stats
Faraday.get "some-domain-dot-com/stats"
end
end
This currently gives me the error: Faraday does not implement: get
So How do you stub this with faraday? I have seen methods like:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get('http://stats-api.com') { [200, {}, 'Lorem ipsum'] }
end
But I can't seem to apply it the right way. What am I missing here?
Note that Faraday.new returns an instance of Faraday::Connection, not Faraday. So you can try using
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return("some data")
Note that I don't know if returning "some data" as shown in your question is correct, because Faraday::Connection.get should return a response object, which would include the body and status code instead of a string. You might try something like this:
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double("response", status: 200, body: "some data")
)
Here's a rails console that shows the class you get back from Faraday.new
$ rails c
Loading development environment (Rails 4.1.5)
2.1.2 :001 > fara = Faraday.new
=> #<Faraday::Connection:0x0000010abcdd28 #parallel_manager=nil, #headers={"User-Agent"=>"Faraday v0.9.1"}, #params={}, #options=#<Faraday::RequestOptions (empty)>, #ssl=#<Faraday::SSLOptions (empty)>, #default_parallel_manager=nil, #builder=#<Faraday::RackBuilder:0x0000010abcd990 #handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, #url_prefix=#<URI::HTTP:0x0000010abcd378 URL:http:/>, #proxy=nil>
2.1.2 :002 > fara.class
=> Faraday::Connection
Coming to this late, but incase anyone else is too, this is what worked for me - a combination of the approaches above:
let(:json_data) { File.read Rails.root.join("..", "fixtures", "ror", "501100000267.json") }
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double(Faraday::Response, status: 200, body: json_data, success?: true)
)
end
Faraday the class has no get method, only the instance does. Since you are using this in a class method what you can do is something like this:
class Judge
def self.stats
connection.get "some-domain-dot-com/stats"
end
def self.connection=(val)
#connection = val
end
def self.connection
#connection ||= Faraday.new(some stuff to build up connection)
end
end
Then in your test you can just set up a double:
let(:connection) { double :connection, get: nil }
before do
allow(connection).to receive(:get).with("some-domain-dot-com/stats").and_return('some data')
Judge.connection = connection
end
I ran into the same problem with Faraday::Adapter::Test::Stubs erroring with Faraday does not implement: get. It seems you need to set stubs to a Faraday adapter, like so:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get("some-domain-dot-com/stats") { |env| [200, {}, 'egg'] }
end
test = Faraday.new do |builder|
builder.adapter :test, stubs
end
allow(Faraday).to receive(:new).and_return(test)
expect(Judge.stats.body).to eq "egg"
expect(Judge.stats.status).to eq 200
A better way to do this, rather than using allow_any_instance_of, is to set the default connection for Faraday, so that Faraday.get will use the connection you setup in your tests.
For example:
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
before do
stubs.get('/maps/api/place/details/json') do |_env|
[
200,
{ 'Content-Type': 'application/json' },
{ 'result' => { 'photos' => [] } }.to_json
]
end
Faraday.default_connection = conn
end
after do
Faraday.default_connection = nil
end

Resources