Getting Rspec unit test coverage with Rails and PostgreSQL - ruby-on-rails

I am trying to write a unit test for the following model concern...
require 'active_support/concern'
module Streamable
extend ActiveSupport::Concern
def stream_query_rows(sql_query, options = 'WITH CSV HEADER')
conn = ActiveRecord::Base.connection.raw_connection
conn.copy_data("COPY (#{sql_query}) TO STDOUT #{options};") do
binding.pry
while row = conn.get_copy_data
binding.pry
yield row
end
end
end
end
So far I have battling this with the following spec...
context 'streamable' do
it 'is present' do
expect(described_class.respond_to?(:stream_query_rows)).to eq(true)
end
context '#stream_query_rows', focus: true do
let(:sql_query) { 'TESTQRY' }
let(:sql_query_options) { 'WITH CSV HEADER' }
let(:raw_connection) do
Class.new do
def self.copy_data(args)
yield
end
def self.get_copy_data
return Proc.new { puts 'TEST' }
end
end
end
before do
allow(ActiveRecord::Base).to receive_message_chain(:connection, :raw_connection).and_return(raw_connection)
described_class.stream_query_rows(sql_query)
end
it 'streams data from the db' do
expect(raw_connection).to receive(:copy_data).with("COPY (#{sql_query}) TO STDOUT #{sql_query_options};")
end
end
end
While I can get the first expect to pass, meaning, I can trigger the first binding.pry, no matter what I try, I can not seem to get past the second.
This is the error...
LocalJumpError:
no block given (yield)
I am only trying to unit test this and ideally not hit the db, only testing the communication of the objects. This also, can and will be used in many models as an option for streaming data.
Reference article: https://shift.infinite.red/fast-csv-report-generation-with-postgres-in-rails-d444d9b915ab
Does anyone have an pointers on how to finish this stub and or adjust the spec so I have the following block covered?
while row = conn.get_copy_data
binding.pry
yield row
end
ANSWER
After reviewing the comments and suggestions below, I was able to refactor the spec and now have 100% coverage.
context '#stream_query_rows' do
let(:sql_query) { 'TESTQRY' }
let(:sql_query_options) { 'WITH CSV HEADER' }
let(:raw_connection) { double('RawConnection') }
let(:stream_query_rows) do
described_class.stream_query_rows(sql_query) do
puts sql_query
break
end
end
before do
allow(raw_connection).to receive(:copy_data).with("COPY (#{sql_query}) TO STDOUT #{sql_query_options};"){ |&block| block.call }
allow(raw_connection).to receive(:get_copy_data).and_return(sql_query)
allow(ActiveRecord::Base).to receive_message_chain(:connection, :raw_connection).and_return(raw_connection)
end
it 'streams data from the db' do
expect(raw_connection).to receive(:copy_data).with("COPY (#{sql_query}) TO STDOUT #{sql_query_options};")
stream_query_rows
end
it 'yields correct data' do
expect { stream_query_rows }.to output("#{sql_query}\n").to_stdout_from_any_process
end
end

Like the error says, you're yielding, but you haven't supplied a block for it to call.
If your method expects a block, then you need to supply one when you call it.
To do that, you need to change this line:
described_class.stream_query_rows(sql_query)
to something like this:
described_class.stream_query_rows(sql_query) { puts "this is a block" }

Related

How to skip part of method code in Rails specs

I've got this method
def finalize_inquiry_process(form)
if finalize_process == true
inquiry_process.campaign_code.update(state: 'used')
document_creator_class = InquiryProcessDocumentCreatorFetcher.new(inquiry_process).call
document_creator_class.new(inquiry_process).call
end
Success(form)
end
and I want to skip in specs this part which is really trouble maker, implementation is an unnecessary waste of time (pdf generator with tons of fields)
document_creator_class = InquiryProcessDocumentCreatorFetcher.new(inquiry_process).call
document_creator_class.new(inquiry_process).call
To do so I wrote a specs:
let(:fetcher_instance) { instance_double(InquiryProcessDocumentCreatorFetcher) }
before do
allow(InquiryProcessDocumentCreatorFetcher).to receive(:new).and_return(fetcher_instance)
allow(fetcher_instance).to receive(:call).and_return(nil)
end
it 'updates state of assigned campain code' do
updated_inquiry_process = process_update.value!
expect(updated_inquiry_process.campaign_code.state).to eq('used')
end
end
InquiryProcesses::Update.call campain code updates state of assigned campain code
Failure/Error: document_creator_class.new(inquiry_process).call
NoMethodError:
undefined method `new' for nil:NilClass
Is there any chance to skip this part of code in specs?
Ok I managed it by using receive_message_chain helper. Specs should looked like this:
describe 'finalize inquiry process' do
subject(:process_update) do
described_class.new(
inquiry_process: inquiry_process,
form: loan_application_inquiry_process_update_form,
finalize_process: true,
).call
end
let!(:inquiry_process) do
create :inquiry_process, inquiry_template: loan_inquiry_template, campaign_code_uid: campaign_code.uid
end
before do
allow(InquiryProcessDocumentCreatorFetcher).to receive_message_chain(:new, :call, :new, :call)
end
it 'updates state of assigned campain code' do
updated_inquiry_process = process_update.value!
expect(updated_inquiry_process.campaign_code.state).to eq('used')
end
end
You can try your luck with dependency injection:
(It's a rough sketch, but I don't know the context of the system, not even the whole class)
def initialize(inquiry_process:, form:, finalize_process:, creator_fetcher:)
#creator_fetcher = creator_fetcher
# all the other initializetions
# maybe you can initialize creator_fetcher outside, and no need to pass inquiry_process anymore?
end
def creator_fetcher
#creator_fetcher ||= InquiryProcessDocumentCreatorFetcher.new(inquiry_process)
end
def finalize_inquiry_process(form)
if finalize_process == true
inquiry_process.campaign_code.update(state: 'used')
document_creator_class = creator_fetcher.call
document_creator_class.new(inquiry_process).call
end
Success(form)
end
and then
let(:creator_fetcher) { instance_double(InquiryProcessDocumentCreatorFetcher) }
let(:document_creator_class) { instance_double(whatever_fetcher_call_returns_or_just_unveryfying_double) }
before { allow(creator_fetcher.to_receive(:call).and_return(document_creator_class) }
subject(:process_update) do
described_class.new(
inquiry_process: inquiry_process,
form: loan_application_inquiry_process_update_form,
finalize_process: true,
).call
end
Anyway - your problems with tests show's that your code was not written with the tests, and it's a bad design.
Indirection (Dependency Injection here) might help a little to untangle the mess.

What a test should test with Rspec in Ruby on Rails application

I'm beginner in Rspec, and actually we asked me to do Rspec test for somes method which are already build but which never have been test (they don't write test before building the method).
And now I'm struggling to know how can I test my method, here is example:
class ConnectorJob < ActiveJob::Base
queue_as :connector
def perform(tenant_id = nil, debug = false)
tenant_to_sync = Tenant.to_sync(tenant_id)
return if tenant_to_sync.empty?
tenant_to_sync.each do |tenant|
service = MyAPP::ContactToSync.new(tenant, debug).call
if service.success?
ConnectorService::Synchronization.new(
tenant, service.data, debug
).call
end
end
end
end
What should I test on this? Should I test the return value is correct or if other method are well called?
Here is what I tried to do
require "rails_helper"
RSpec.describe ConnectorJob, type: :job do
it 'is in connector queue' do
expect(ConnectorJob.new.queue_name).to eq('connector')
end
describe 'perform' do
let (:tenant) { create(:tenant) }
let (:job) { ConnectorJob.new.perform(tenant.id) }
context 'with empty tenant' do
it { expect(ConnectorJob.new.perform(#tenant.id)).to eq nil }
end
context 'with tenant' do
it { expect(ConnectorJob.new.perform(tenant.id)).to eq job }
end
end
end
As you can see my last test doesn't have sense but I have no idea what I should write on my Rspec for anticipate the result of this method.
If I check my Rspec coverage, Rspec is telling me I cover 100% of my method but I'm not sure that is correct.
I hope I'm clear, feel free to ask me more details.
Thank you all
I think you should test final result, I mean result after calling
ConnectorService::Synchronization.new(...).call and test three cases, e.g. if this call create new user, you should test it:
If tenant_to_sync.empty? == true
context 'with empty tenant' do
it { expect(ConnectorJob.new.perform(#tenant.id)).to change(User.count).by(0) }
end
If service.success? == false
context 'MyAPP::ContactToSync return false' do
it { expect(ConnectorJob.new.perform(#tenant.id)).to change(User.count).by(0) }
end
If service.success? == true
context 'success' do
it { expect(ConnectorJob.new.perform(#tenant.id)).to change(User.count).by(1) }
end
It should be enough to cover all scenarios.

Test if a method is called inside a module method

I have the following in my module:
module SimilarityMachine
...
def answers_similarity(answer_1, answer_2)
if answer_1.compilation_error? && answer_2.compilation_error?
return compiler_output_similarity(answer_1, answer_2)
elsif answer_1.compilation_error? || answer_2.compilation_error?
return source_code_similarity(answer_1, answer_2)
else
content_sim = source_code_similarity(answer_1, answer_2)
test_cases_sim = test_cases_output_similarity(answer_1, answer_2)
answers_formula(content_sim, test_cases_sim)
end
end
...
end
I would like to test these "if conditions", to ensure that the right methods are called (all these methods are from SimilarityMachine module). To do that, I have:
describe SimilarityMachine do
describe '#answers_similarity' do
subject { answers_similarity(answer_1, answer_2) }
let(:answer_1) { create(:answer, :invalid_content) }
context "when both answers have compilation error" do
let(:answer_2) { create(:answer, :invalid_content) }
it "calls compiler_output_similarity method" do
expect(described_class).to receive(:compiler_output_similarity)
subject
end
end
end
With both answers created I go to the right if (the first, and I'm sure of that because I tested before). However, my result is:
1) SimilarityMachine#answers_similarity when both answers have compilation error calls compiler_output_similarity method
Failure/Error: expect(described_class).to receive(:compiler_output_similarity)
(SimilarityMachine).compiler_output_similarity(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
What am I doing wrong?
I would check out Testing modules in rspec other questions related to testing modules.
I'm not completely clear on this, but in general, modules don't receive method calls. They are collections of methods that have to be "mixed in" through the extend method and the like.
Here's an example how to test a module method in isolation, taken from https://semaphoreci.com/community/tutorials/testing-mixins-in-isolation-with-minitest-and-rspec:
describe FastCar
before(:each) do
#test_obj = Object.new
#test_obj.extend(Speedable)
end
it "reports the speed" do
expect(#test_obj.speed).to eq "This car runs super fast!"
end
end

How to test that a method gets called in a do-end block in rspec

I have a method that I am writing a test for. I want to test that generate_csv_header and generate_csv_line gets called.
def generate_temporary_report_file(temporary_file)
CSV.open(temporary_file, "wb") do |csv|
csv << generate_csv_header
csv << generate_csv_line
end
temporary_file
end
Here is my test so far:
describe '#generate_temporary_report_file' do
let(:temp_file) { double(:temp_file, read: 'content') }
let(:csv_open) { double(:csv_open) }
before do
expect(CSV).to receive(:open).with(temp_file, "wb").and_return(csv_open)
# expect(item_report_generator).to receive(:generate_csv_header).once
# expect(item_report_generator).to receive(:generate_csv_line).once
end
it 'returns temporary file' do
expect(item_report_generator.generate_temporary_report_file(temp_file)).to eq temp_file
end
end
I commented out two lines because those methods are getting stubbed out.
How do I test that generate_csv_header and generate_csv_line gets called once?
Thanks!

newbie can't define a method in ruby for cucumber test pass

I am trying to learn cucumber, here's an example code from a book:
class Output
def messages
#messages ||= []
end
def puts(message)
messages << message
end
end
def output
#output ||= Output.new
end
Given /^I am not yet playing$/ do
end
When /^I start a new game$/ do
game = Codebreaker::Game.new(output)
game.start
end
Then /^I should see "([^"]*)"$/ do |message|
output.messages.should include(message)
end
When I run this spec, I get this error:
Scenario: start game # features/codebreaker_starts_game.feature:7
Given I am not yet playing # features/step_definitions/codebreaker_steps.rb:15
When I start a new game # features/step_definitions/codebreaker_steps.rb:18
Then I should see "Welcome to Codebreaker!" # features/step_definitions/codebreaker_steps.rb:23
undefined method `messages' for #<RSpec::Matchers::BuiltIn::Output:0xa86a7a4> (NoMethodError)
./features/step_definitions/codebreaker_steps.rb:24:in `/^I should see "([^"]*)"$/'
features/codebreaker_starts_game.feature:10:in `Then I should see "Welcome to Codebreaker!"'
And I should see "Enter guess:" # features/step_definitions/codebreaker_steps.rb:23
See that it gives undefined method 'messages' error, yet it is defined in the Output class.
If I replace output.messages.should with Output.new.messages.should, it works fine. What is the problem here?
Edit: Probably output is a keyword, in new version of rails, when I changed it to outputz it worked fine. An explanation of this will be accepted as an answer.
Apparently, the output matcher has been added to rspec in version 3.0:
The output matcher provides a way to assert that the has emitted
content to either $stdout or $stderr.
With no arg, passes if the block outputs to_stdout or to_stderr. With
a string, passes if the blocks outputs that specific string to_stdout
or to_stderr. With a regexp or matcher, passes if the blocks outputs a
string to_stdout or to_stderr that matches.
Examples:
RSpec.describe "output.to_stdout matcher" do
specify { expect { print('foo') }.to output.to_stdout }
specify { expect { print('foo') }.to output('foo').to_stdout }
specify { expect { print('foo') }.to output(/foo/).to_stdout }
specify { expect { }.to_not output.to_stdout }
specify { expect { print('foo') }.to_not output('bar').to_stdout }
specify { expect { print('foo') }.to_not output(/bar/).to_stdout }

Resources