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 }
Related
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/
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.
class ExternalObject
attr_accessor :external_object_attribute
def update_external_attribute(options = {})
self.external_object_attribute = [1,nil].sample
end
end
class A
attr_reader :my_attr, :external_obj
def initialize(external_obj)
#external_obj = external_obj
end
def main_method(options = {})
case options[:key]
when :my_key
self.my_private_method(:my_key) do
external_obj.update_external_attribute(reevaluate: true)
end
else
nil
end
end
private
def my_private_method(key)
old_value = key
external_object.external_object_attribute = nil
yield
external_object.external_object_attribute = old_value if external_object.external_object_attribute.nil?
end
end
I want to test following for main_method when options[:key] == :my_key:
my_private_method is called once with argument :my_key and it has a block {external_obj.update_external_attribute(reevaluate: true) } , which calls update_external_attribute on external_obj with argument reevaluate: true once.
I'm able to test my_private_method call with :my_key argument once.
expect(subject).to receive(:my_private_method).with(:my_key).once
But how do I test the remaining part of the expectation?
Thank you
It could be easier to answer your question if you post your test as well.
The setup, the execution and asseriotns/expectations.
You can find a short answer in this older question.
You can find useful to read about yield matchers.
I would suggest to mock the ExternalObject if you already haven't. But I can't tell unless you post your actual test code.
I'm going to answer your question. But, then I'm going to explain why you should not do it that way, and show you a better way.
In your test setup, you need to allow the double to yield so that the code will fall through to your block.
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(a).to receive(:my_private_method).and_yield
allow(external_obj).to receive(:update_external_attribute)
main_method
end
it 'does something useful' do
expect(a)
.to have_received(:my_private_method)
.with(:my_key)
.once
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
That works. The test passes. RSpec is a powerful tool. And, it will let you get away with that. But, that doesn't mean you should. Testing a private method is ALWAYS a bad idea.
Tests should only test the public interface of a class. Otherwise, you'll lock yourself into the current implementation causing the test to fail when you refactor the internal workings of the class - even if you have not changed the externally visible behavior of the object.
Here's a better approach:
RSpec.describe A do
subject(:a) { described_class.new(external_obj) }
let(:external_obj) { instance_double(ExternalObject) }
describe '#main_method' do
subject(:main_method) { a.main_method(options) }
let(:options) { { key: :my_key } }
before do
allow(external_obj).to receive(:update_external_attribute)
allow(external_obj).to receive(:external_object_attribute=)
allow(external_obj).to receive(:external_object_attribute)
main_method
end
it 'updates external attribute' do
expect(external_obj)
.to have_received(:update_external_attribute)
.with(reevaluate: true)
.once
end
end
end
Note that the expectation about the private method is gone. Now, the test is only relying on the public interface of class A and class ExternalObject.
Hope that helps.
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
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.