A more readable request spec that changes model count - ruby-on-rails

I have the following rspec request spec example which passes
it "increases count by 1" do
attributes = attributes_for(:district)
expect { post admin_districts_path, params: { district: attributes} }.to change { District.count }.by(1)
end
The expect line is a little busy, so I'm trying to break it up. The following causes an error
it "increases count by 1" do
attributes = attributes_for(:district)
block = { post admin_districts_path, params: { district: attributes} }
expect(block).to change { District.count }.by(1)
end
with error
syntax error, unexpected '}', expecting keyword_end
Why is this error happening? Is there a cleaner way to write this spec example?

I usually run into this kind of long lines in tests. Instead of creating new variables just to improve reading, what I do is to split it into different lines like this:
it "increases count by 1" do
attributes = attributes_for(:district)
expect do
post admin_districts_path, params: { district: attributes}
end.to change { District.count }.by(1)
end

Also, you can create a lambda:
block = -> { post admin_districts_path, params: { district: attributes} }
expect(block).to change { District.count }.by(1)

I prefer to create a little helper method within the relevant describe block. This is taken from a sample rails request spec, and I'm assuming you have FactoryBot set up. Something like this:
describe "create /district" do
def create_district_request
#district = build(:district)
params = {district: {name: #district.name etc.}}
post district_path, params: params
end
it "creates a district" do
expect {create_district_request}.to change{District.count}.by(1)
end
end
Hope it helps.

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/

What is an equivalent to database_cleaner in Rails 6?

I have a spec, with an object and two contexts. In one context I set one key to nil and in the other not:
describe SomeClass::SomeService, type: :model do
describe '#some_method' do
subject { described_class.new(params, current_user).some_method }
mocked_params = {
min_price: 0,
max_price: 100
}
let(:params) { mocked_params }
let(:current_user) { User.create(email: 'name#mail.com') }
context 'with invalid params' do
it 'returns nil if any param is nil' do
params[:min_price] = nil
expect(subject).to eq(nil)
end
end
context 'with valid params' do
it 'returns filtered objects' do
expect(subject).to eq([])
end
end
end
end
The problem is that the second test fails because min_price is still nil.
I read that from Rails 5 on I don't need database_cleaner. Do I need it or not?
I thought that the let method creates a new object every time it sees the variable. Since I have two contexts, and the subject method is called in both of them, and inside the subject I have the variable params, why is the params object not a new one with all the fields at every context?
I read that from rails 5 on I don't need database_cleaner. Do I need
or not?
No. It's no longer needed. In previous versions of Rails the database transaction method of rolling back changes only worked (at times) with TestUnit/Minitest and fixtures.
I thought that the let method creates a new object every time it sees
the variable. Since I have two context, and the subject method is
called in both of them, and inside the subject I have the variable
params, why the params object is not a new one at every context? (with
all the fields)
This is completely wrong.
Use let to define a memoized helper method. The value will be cached
across multiple calls in the same example but not across examples.
When you do:
mocked_params = {
min_price: 0,
max_price: 100
}
let(:params) { mocked_params }
You're really just returning a reference to the object mocked_params and then mutating that object.
If you do:
let(:params) do
{
min_price: 0,
max_price: 100
}
end
You will get a new hash object on the first call to let and the value will then be cached but not shared between examples. But that's really the tip of the iceberg with this spec.
describe SomeClass::SomeService, type: :model do
describe '#some_method' do
let(:current_user) { User.create(email: 'name#mail.com') }
# explicit use of subject is a serious code smell!
let(:service) { described_class.new(params, current_user) }
context 'with invalid params' do
# since let is lazy loading we can define it in this context instead
let(:params) do
{
min_price: nil,
max_price: 100
}
end
it 'returns nil if any param is nil' do
# actually call the method under test instead of misusing subject
# this makes it much clearer to readers what you are actually testing
expect(service.some_method).to eq(nil)
end
end
context 'with valid params' do
let(:params) do
{
min_price: 0,
max_price: 100
}
end
it 'returns filtered objects' do
expect(service.some_method).to eq([])
end
end
end
end
Its also pretty questionable why the object under test takes a hash as the first positional parameter and not as the last parameter which is the Ruby way.
This happens because you initialize the mocked_params only once when the file is loaded and then you change that hash in the first test.
Instead create the params within the let block which would lead to a re-creation of the hash for each test.
Change
mocked_params = {
min_price: 0,
max_price: 100
}
let(:params) { mocked_params }
to
let(:params) do
{
min_price: 0,
max_price: 100
}
end

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.

Issue with Faker gem

I installed Fabrication and Faker in my Rails 4 project
I created a fabrarication object:
Fabricator(:course) do
title { Faker::Lorem.words(5) }
description { Faker::Lorem.paragraph(2) }
end
And I'm calling the Faker object within my courses_controller_spec.rb test:
require 'spec_helper'
describe CoursesController do
describe "GET #show" do
it "set #course" do
course = Fabricate(:course)
get :show, id: course.id
expect(assigns(:course)).to eq(course)
end
it "renders the show template"
end
end
But for some reason, the test failes at line 6:
course = Fabricate(:course)
the error message is:
Failure/Error: course = Fabricate(:course)
TypeError:
can't cast Array to string
Don't know exactly why this is failing. Has anyone experienced the same error message with Faker?
The return from words(5) is an array, not a string. You should do this:
title { Faker::Lorem.words(5).join(" ") }
Not sure if this was available back then, but another solution would be to give it a sentence instead of a word:
From the repo
def sentence(word_count = 4, supplemental = false, random_words_to_add = 6)
words(word_count + rand(random_words_to_add.to_i).to_i, supplemental).join(' ').capitalize + '.'
end
So you could do:
Faker::Lorem.sentence(5, false, 0)

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