How to properly mock objects in RSpec? - ruby

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

Related

How to inspect CSV file columns inside RSpec test?

I really don't know how to inspect CSV file I created in my ROR App.
require "rails_helper"
require "shared_contexts/vcr/s3"
require "csv"
RSpec.describe ReportRuns::RunService do
describe "CSV columns" do
include_context "vcr s3 put csv"
let(:report_run) { create :report_run, report_template: report_template, created_by: user.id, mime_type: "csv" }
#let(:report_template) { create :report_template, template_structure: { module: "trial_members", filters: { trial_members: [trial_members.id] } } }
let(:report_template) { create :report_template, trial: trial }
let(:trial) { create :trial }
let(:user) { create :user }
let(:user_role) { create :user_role }
subject { described_class.new(report_run) }
before do
end
it do
get :index, format: :csv
p "response jee: #{response.body}"
p response.headers
p "report run: #{report_run.inspect}"
p "templejt: #{report_template.inspect}"
p "mime type: #{report_run[:mime_type]}"
#p "trila je: #{trial.inspect}"
p "users are: #{user.inspect}"
p "user roles su: #{user_role.inspect}"
is_expected.to be_truthy
expect(5).to match(5)
end
end
end
Use the CSV library to parse the body of the response. Then work with the CSV object.
csv = CSV.new(response.body)
You can also check the Content-type headers are correct, text/csv.

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

RSpec spies work differently on two different classes

I have a class Uploader which takes a file and uploads it to S3. I'm trying to test that #s3 is actually receiving a file body when upload_file is called. When I test that File is getting messages sent, the test passes. However, trying to spy on Aws::S3::Client does not work.
class Uploader
def initialize(tmp_dir_name, bucket)
#base_tmp_dir = tmp_dir_name
#s3 = Aws::S3::Client.new(region: 'us-east-1')
#bucket = bucket
#uploaded_assets = []
end
def upload_file(key, file_path)
file = File.new(file_path)
#s3.put_object(bucket: #bucket, key: key.to_s, body: file.read)
end
end
RSpec.describe Uploader do
let(:bucket) { 'test_bucket' }
let(:base_temp_dir) { 'test_temp_dir' }
let(:uploader) { Uploader.new(base_temp_dir, bucket) }
describe "#upload_file" do
let(:file) { double('file') }
before { allow(File).to receive(:new) { file } }
before { allow(file).to receive(:read).and_return('text') }
before { allow(Aws::S3::Client).to receive(:put_object) }
it "uses one file" do
uploader.upload_file('test_key', 'file_path')
expect(File).to have_received(:new).with('file_path')
end
it "sends data to s3" do
uploader.upload_file('test_key', 'file_path')
expect(Aws::S3::Client).to have_received(:put_object)
end
end
end
I ended up mocking out s3 for this particular test.
it "sends data to s3" do
test_key = 'test_key'
bucket = 'test_bucket'
fake_s3 = instance_double(Aws::S3::Client)
allow(Aws::S3::Client).to receive(:new).and_return(fake_s3)
allow(fake_s3).to receive(:put_object)
uploader.upload_file(test_key, 'file_path', record=true)
expect(fake_s3).to have_received(:put_object).with(
{bucket: bucket, key: test_key, body: 'text'})
end

expect_any_instance_of with an object argument

I am testing a class's initialization block as below
class A
attr_accessor :client
def initialize(options, configuration)
self.client = B.new(options)
config = C.new(
url: configuration[:url],
headers: configuration[:headers],
username: configuration[:username],
password: configuration[:password]
)
client.configure(config)
end
end
class C
def initialize(options)
# does something with options hash
end
end
class B
def initialize(options)
# does something with options hash
end
def configure(config)
# some configuration with config object
end
end
My test case is as follows:
let(:options) {
{
force_basic_auth: true
}
}
let(:configuration) {
{
url: 'https://localhost:3000',
headers: { awesome: true },
username: 'test',
password: 'pass'
}
}
let(:api_config) {
C.new(configuration)
}
it 'configures object with passed params' do
expect_any_instance_of(B).to receive(:configure)
.with(api_config)
A.new(
options,
configuration
)
end
This fails my test case because the object that is created in the initialization block has a different object_id than the object_id of api_config which I am using in the expectations.
-[#<C:0x00000002b51128 #url="https://localhost:3000", #headers={:awesome=>true}, #username="test", #password="pass">]
+[#<C:0x00000002a1b628 #url="https://localhost:3000", #headers={:awesome=>true}, #username="test", #password="pass">]
Seeing that failure I was thinking whether it's a best practice to pass such objects directly in the initialization block. I mean I can fix it by directly passing the object in the initialization block.
There are many functions which are initializing the A class with a hash option being passed because of which I am doing it in the current way.
Is there a way to expect the contents of the object passed in rspec instead of verifying the objects are same ? Is passing the object directly in the initialization a more better approach ?
You can define arbitrary expectation handling to check the value of the parameter checked (see here):
it 'configures object with passed params' do
expect_any_instance_of(B).to receive(:configure) do |config|
expect(config).to be_a(C)
expect(config.url).to eq(configuration[:url])
expect(config.headers).to eq(configuration[:headers])
# ...
end
A.new(
options,
configuration
)
end
You want the configuration hash (rather than the object) under B.configure(config), so your class has to change slightly to accommodate.
Class file
class A
attr_accessor :client
def initialize(options, configuration)
self.client = B.new(options)
config = C.new(
url: configuration[:url],
headers: configuration[:headers],
username: configuration[:username],
password: configuration[:password]
)
client.configure(config.options)
end
end
class C
attr_reader :options
def initialize(options)
#options = options
end
end
class B
def initialize(options)
# does something with options hash
end
def configure(config)
# some configuration with config object
end
end
Here's what your RSpec code would look like.
describe do
let(:options) do
{
force_basic_auth: true
}
end
let(:configuration) do
{
url: 'https://localhost:3000',
headers: { awesome: true },
username: 'test',
password: 'pass'
}
end
let(:my_a_object) { A.new(options, configuration) }
let(:my_b_object) { B.new(options) }
it 'configures object with passed params' do
allow(B).to receive(:new).with(options).and_return(my_b_object)
expect(my_b_object).to receive(:configure).with(configuration)
my_a_object
end
end

How to mock a method call in Rspec

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

Resources