Refactoring Rspec specs - ruby-on-rails

I am trying to cleanup my specs as they are becoming extremely repetitive.
I have the following spec
describe "Countries API" do
it "should render a country list" do
co1 = Factory(:country)
co2 = Factory(:country)
result = invoke :GetCountryList, "empty_auth"
result.should be_an_instance_of(Api::GetCountryListReply)
result.status.should be_an_instance_of(Api::SoapStatus)
result.status.code.should eql 0
result.status.errors.should be_an_instance_of Array
result.status.errors.length.should eql 0
result.country_list.should be_an_instance_of Array
result.country_list.first.should be_an_instance_of(Api::Country)
result.country_list.should have(2).items
end
it_should_behave_like "All Web Services"
it "should render a non-zero status for an invalid request"
end
The block of code that checks the status will appear in all of my specs for 50-60 APIs. My first thought was to move that to a method and that refactoring certainly makes things much drier as follows :-
def status_should_be_valid(status)
status.should be_an_instance_of(Api::SoapStatus)
status.code.should eql 0
status.errors.should be_an_instance_of Array
status.errors.length.should eql 0
end
describe "Countries API" do
it "should render a country list" do
co1 = Factory(:country)
co2 = Factory(:country)
result = invoke :GetCountryList, "empty_auth"
result.should be_an_instance_of(Api::GetCountryListReply)
status_should_be_valid(result.status)
result.country_list.should be_an_instance_of Array
result.country_list.first.should be_an_instance_of(Api::Country)
result.country_list.should have(2).items
end
end
This works however I can not help feeling that this is not the "right" way to do it and I should be using shared specs, however looking at the method for defining shared specs I can not easily see how I would refactor this example to use a shared spec.
How would I do this with shared specs and without having to re-run the relatively costly block at the beginning namely
co1 = Factory(:country)
co2 = Factory(:country)
result = invoke :GetCountryList, "empty_auth"

Here's one option, using the new-ish "subject" feature of RSpec. Note that this will run the before :all block twice, once for each nested "describe" block. If this ends up being too slow, you can flatten things out at the cost of not being able to use the "subject" syntax for the status shared examples (since subject applies to the entire describe block it's used in).
shared_examples_for "valid status" do
it { should be_an_instance_of(Api::SoapStatus) }
its(:code) { should eql(0) }
its(:errors) { should be_an_instance_of(Array) }
its(:errors) { should be_empty }
end
describe "Countries API" do
before :all do
co1 = Factory(:country)
co2 = Factory(:country)
#result = invoke :GetCountryList, "empty_auth"
end
subject { #result }
it { should be_an_instance_of(Api::GetCountryListReply) }
its(:country_list) { should be_an_instance_of (Array) }
it "should have countries in the country list" do
#result.country_list.each {|c| c.should be_an_instance_of(Api::Country)}
end
its(:country_list) { should have(2).items }
describe "result status" do
subject { #result.status }
it_should_behave_like "valid status"
end
end

Related

Rails Rspec - How to test if Service has been called in another Service

While writing tests, I stopped at trying to test Service in another Service. In such a situation, I should probably just check if Service has been called because it has already been tested elsewhere. I did a little research on the Internet and found something like have_received but I have no idea how to use it in my example.
check_service.rb
Class CheckService
def initialize(params)
#params = params
end
def self.call(params)
new(params).call
end
def call
CheckUser.call(params[:user_id])
end
end
check_service_spec.rb
...
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
let(:check_user) { instance_double(CheckUser) }
before do
allow(check_user).to receive(:call).and_return(true)
end
it do
result
expect(check_user).to have_received(:call)
end
end
...
I was trying something like this (it's simple example), but I get error:
(InstanceDouble(CheckUser) (anonymous)).call(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
Is there any option to test situation I presented?
Short anwser
describe 'call' do
let(:result) { CheckService.call(params) }
let(:params) { { user_id: "100" } }
## let(:check_user) { instance_double(CheckUser) } delete this
before do
allow(CheckUser).to receive(:call).and_return(true)
end
it do
result
expect(CheckUser).to have_received(:call)
end
end
Alternative
I think a better way to test this is to use DI (Dependency Injection), so you pass CheckUser as a dependency to CheckService. I prefer to write the whole test inside the it block too!
class CheckService
def initialize(params, check_handler:)
#params = params
#check_handler = check_handler
end
def self.call(params, check_handler: CheckUser)
new(params, check_handler: check_handler).call
end
def call
#check_handler.call(#params[:user_id])
end
end
describe 'call' do
it 'check user with params' do
check_user = class_double(CheckUser)
allow(check_user).to receive(:call).and_return(true)
params = { user_id: "100" }
CheckService.call(params, check_handler: check_user)
expect(check_user).to have_received(:call)
end
end
A blog post to read more about -> https://blog.testdouble.com/posts/2018-05-17-do-we-need-dependency-injection-in-ruby/

how to mock method retry and variable assignment inside method in rspec

require 'requiredclass'
class Test
def get_client()
return some_client
end
def intermediate_method()
res = nil
self.class
.get_client
.retry(tries:5, on: [RequiredClass::ClientTimeout]) do |myclient|
call_count += 1
res = myclient.dosomething()
end
return res
end
def method_to_test()
x = intermediate_method()
y = false
return x && y
end
end
How can I write rspec for method_to_test here. How can I mock get_client.retry as well as calls to get_client while also mocking res variable assignment so that gets assigned the value i would like it to assign.
As written, this code is difficult to test. That's a smell and a sign that the code should be restructured. Really any time you feel tempted to mock a method in the current class, that's a sign that the thing you want to mock does not belong in that class. It should be injected (passed in) instead. Like this:
require 'requiredclass'
class Test
attr_reader :client
def initialize(client)
#client = client
end
def method_to_test
x = intermediate_method
y = false
x && y
end
def intermediate_method
res = nil
client.retry(tries: 5, on: [RequiredClass:ClientTimeout]) do |my_client|
call_count += 1
res = my_client.do_something
end
res
end
end
Given this refactored code, the tests might look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client, retry: true, do_something: true) }
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
In this code I've passed a double with a stubbed retry method into the Test class on instantiation. You could optionally use a mock, instead. That would look like this:
RSpec.describe Test do
subject(:test) { Test.new(client) }
let(:client) { instance_double(Client) }
before do
allow(client).to receive(:retry)
allow(client).to receive(:do_something)
end
describe '#method_to_test'
subject(:method_to_test) { test.method_to_test }
it 'returns false' do
expect(method_to_test).to be_false
end
end
end
There's a good write up of mocks and doubles in the RSpec documentation.

How to know the flow of the controller method using Rspec

I have two dependent drop down.One gives me orgname and other drop down populates on selecting a orgname, That is teamname.
This is my github_leader_board_spec.rb
describe "github_leader_board" do
before do
#obj = DashboardsController.new
end
context "with session" do
subject { get :github_leader_board, :params => { :orgname => "test", :teamname=> "team"}}
it "returns http success" do
expect(response).to have_http_status(:success)
end
it "executes other functions" do
expect(#org_data).not_to be_nil
expect(#obj.get_team_api("DevCenter")).not_to be_nil
end
end
end
This is my controller method
def github_leader_board
myhash = {}
#points_hash = {}
member_data = []
#org_data = get_org_api
#orgs = get_names(org_data)
team_data = get_team_api(params[:orgname])
#teams = get_names(team_data)
teamid = get_team_id(team_data)
#teams.each_with_index {|k,i|myhash[k] = teamid[i]}
myhash.each do |key,value|
if key == params[:teamname]
member_data = get_members("#{value}")
end
end
#memberids = get_names(member_data)
member_names = get_member_names(#memberids)
review_comments = get_reviewcoments(#memberids)
reactions = points(#memberids)
points = [review_comments, reactions].transpose.map {|x| x.reduce(:+)}
member_names.each_with_index {|k,i|#points_hash[k] = points[i]}
end
If i run my spec file it says, undefined #org_data. The function inside the github_leader_board controller is not calling the get_org_api and storing the value to the #org_data variable.
Can anybody suggest what is wrong with the code and how can i improve it. As i'm new to ror.
Any help would be appreciated.
Thank you.
I believe you could use a test of the type controller, instead of instantiating your controller and then use the RSpec method assigns (docs) to test your instance variables, something like this:
RSpec.describe DashboardsController, :type => :controller do
context "with session" do
# ...
it "executes other functions" do
expect(assigns(:org_data)).not_to be_nil
end
end
end
https://relishapp.com/rspec/rspec-rails/docs/controller-specs
Also, if you want to check the flow, and debug your code, you can use the gems pry, pry-rails and pry-nav as #Marek Lipka stated.

Test service Rspec always return empty array

My service work perfectly testing manually, but I need write service test. So I create service test in rspec
require 'rails_helper'
RSpec.describe ReadService do
describe '#read' do
context 'with 4 count' do
let!(:object1) { create(:obj) }
let!(:object2) { create(:obj) }
let!(:object3) { create(:obj) }
let!(:object4) { create(:obj) }
it 'return 2 oldest obj' do
expect(ReadService::read(2)).to eq [report4,report3]
end
end
But ReadService::read(2) in test return []
When I type this manually
ReadService::read(2)
it return array with two oldest obj correctly. What do I wrong? I call this service in test not correctly ?
ReadService implementation
class ReadService
def self.read(count)
objects = Object.get_oldest(count)
objects.to_a
end
end
This happens because you use let!. This helper will only create the object when you first reference it, which you never do in your test. In this case you should rather use a before :each or before :all block (depending on what your specs do in the describe block):
before :each do
#object1 = create :obj
#object2 = create :obj
#object3 = create :obj
#object4 = create :obj
end
If you do not need a reference to the objects, you can create them in a loop:
4.times { create :obj }

Test that method has a default value for an argument with RSpec

How can I test that a method that takes an argument uses a default value if an argument is not provided?
Example
# this method shouldn't error out
# if `Post.page_results` without a parameter
class Post
def self.page_results(page=1)
page_size = 10
start = (page - 1) * page_size
finish = start + page_size
return Page.all[start..finish]
end
end
How do I check in rspec that page equals 1 if page_results is called without argument?
Testing that the page param has the default value set, is most likely not what you should test. In most cases, it is better to test the behaviour instead of the implementation (Talk from Sandy Metz about testing). In your case, you should test if the expected set of Pages is returned, when page_results is called without params (default case).
Here is an example of how you could do this:
describe Post do
describe ".page_results" do
context "when Pages exist" do
subject(:pages) { described_class.page_results(page) }
let(:expected_pages_default) { expected_pages_page_1 }
let(:expected_pages_page_1) { Page.all[0..10] }
let(:expected_pages_page_2) { Page.all[10..20] }
before do
# Create Pages
end
context "when no page param is give " do
# HINT: You need to redefine subject in this case. Setting page to nil would be wrong
subject(:pages) { described_class.page_results }
it { expect(pages).to eq expected_pages_default }
end
context "when the page param is 1" do
let(:page) { 1 }
it { expect(pages).to eq expected_pages_page_1 }
end
context "when the page param is 2" do
let(:page) { 2 }
it { expect(pages).to eq expected_pages_page_2 }
end
end
context "when no Pages exist" do
# ...
end
end
end
describe Post do
describe '#page_results' do
let(:post) { create :post }
context 'without arguments' do
it { post.page_results.should eq 1 }
end
end
end
The create :post statement is how you would do it with FactoryGirl. But you could of course mock or stub out your model.
Update
describe Post do
describe '#page_results' do
context 'without arguments' do
it { Post.page_results.should eq 1 }
end
context 'with arguments' do
it { Post.page_results('foo').should eq 'bar' }
end
end
end
I use before_validation to set defaults e.g
before_validation do
self.some_attribute ||=a_default_value
end
That leaves open the possibility to override the default
SomeClassWithDefaultAttributes.create(some_attribute:a_non_default_value)
Keep in mind that if #before_validation returns false, then the validation will fail

Resources