I've got a few tests like this:
it 'should invite user again' do
admin_user = create(:invited_admin_user)
expect_any_instance_of(AdminUser).to receive(:invite!).and_return(true)
patch :reinvite, params: { id: admin_user.to_param }
end
I really want to write it like this:
it 'should invite user again' do
admin_user = create(:invited_admin_user)
expect(admin_user).to receive(:invite!).and_return(true)
patch :reinvite, params: { id: admin_user.to_param }
end
But the test fails if I do that. Any idea why that would happen? I'm using factory_bot to create the AdminUser instance.
I've tried putting puts statements in the test and the invite method to confirm the ID.
def invite!(_param1 = AdminUser.new, _param2 = {})
puts 'ID in invite!' + self.id.inspect
super(_param1, _param2)
end
it 'should invite user again' do
admin_user = create(:invited_admin_user)
puts 'adminuser created' + admin_user.id.inspect
expect(admin_user).to receive(:invite!).and_return(true)
patch :reinvite, params: { id: admin_user.to_param }
end
Result
adminuser created7768
ID in invite!7768
The problem is that your code is finding the record afresh and instantiating it as a new object, therefore the object that receives the message is different to the one in your test.
To get round this do:
it 'should invite user again' do
admin_user = create(:invited_admin_user)
expect(User).to receive(:find).with(admin_user.id).and_return(admin_user)
expect(admin_user).to receive(:invite!).and_return(true)
patch :reinvite, params: { id: admin_user.to_param }
end
This just intercepts the User.find call and returns your test object rather than the one it would normally initialise.
Why do you want to use mocks here in the first place?
it 'should invite user again' do
admin_user = create(:invited_admin_user)
patch :reinvite, params: { id: admin_user.to_param }
expect(admin_user.reload.invited).to eq(true)
end
If you want to avoid redundant calls to the database, the whole test should be written a) without a real DB object creation (FactoryGirl#build,) b) without patch call (directly call the respective controller’s method,) and c) mocking everything to be called in between.
NB I personally do not see any reason at all to have tests where everything is mocked: they hardly differ from the code itself. I mean, we could make a mistake in the test as well as in the code and checking that patch calls the respective controller’s method is silly: it’s already checked in Rails tests. I always try to test real things, when applicable (like the user was indeed changed, rather than some method was invoked.)
Related
My chat app has a chat class and a message class; when a message is added to the chat, chat.updated_at should also be updated (achieved with belongs_to :chat, touch: true).
When I test this manually, it works correctly, the time is updated. My test below fails however, and I cannot work out why.
test "sending a message should update chats updated timestamp" do
sign_in #user
assert_changes "#chat.updated_at" do
post messages_path(params: { message: {
text: 'Hello', to_id: #bob.id, chat_id: #chat.id
}})
assert_response :success
end
end
I simply get the error #chat.updated_at didn't change.
My chat fixture is
one:
id: 1
subject: nil
updated_at: <%= 2.hours.ago %>
I think you should use model.reload https://apidock.com/rails/ActiveRecord/Persistence/reload
assert_changes "#chat.reload.updated_at" do
Explanation:
Once a Rails model is loaded from DB, when you access an attribute it will use the values that were already read and not make the same query again and again (unless explicitly told to do so with reload). And in your test, Ruby simply compares #chat.updated_at before and after but there is no second query on the second time, simply a cached attribute
You need to reload the record from the database to get the object in your test to reflect the changes that were performed in the database:
test "sending a message should update chats updated timestamp" do
sign_in #user
assert_changes "#chat.updated_at" do
post messages_path(params: { message: {
text: 'Hello', to_id: #bob.id, chat_id: #chat.id
}})
#chat.reload
assert_response :success
end
end
Remember that #chat in your test and controller point to completely different objects in memory.
I want to test that a class receives a class-method call in RSpec:
describe MyObject do
it "should create a new user" do
expect(User).to receive(:new)
MyObject.new.doit
end
end
class MyObject
def doit
u = User.new
u.save
end
end
The problem is that the expectation does not halt execution. It simply stubs the class method .doit and continues execution.
The effect of the expectation is to ensure that User.new returns nil. So when we get to the next line which is User.save it then fails because there is no user object to call .save on.
I would like execution to halt as soon as the RSpec expectation has been satisfied - how can I do that?
nb
This is just an illustrative example - while an expect to change would work for User.new, it's not this actual code that I need to test
There is a great method for this and_call_original:
expect(User).to receive(:new).and_call_original
based on your test description, you're testing that a record was created, in those cases I would suggest you to do this:
expect {
MyObject.new.doit
}.to change{User.count}
or if you want to make sure it only created one:
expect {
MyObject.new.doit
}.to change{User.count}.by(1)
I am trying to write a test for my InvitationsController#Create.
This is a POST http action.
Basically what should happen is, once the post#create is first executed, the first thing that needs to do is we need to check to see if an User exists in the system for the email passed in via params[:email] on the Post request.
I am having a hard time wrapping my head around how I do this.
I will refactor later, but first I want to get the test functionality working.
This is what I have:
describe 'POST #create' do
context 'when invited user IS an existing user' do
before :each do
#users = [
attributes_for(:user),
attributes_for(:user),
attributes_for(:user)
]
end
it 'correctly finds User record of invited user' do
expect {
post :create, invitation: attributes_for(:member, email: #users.first.email)
}.to include(#users.first[:email])
end
end
end
This is the error I get:
1) Users::InvitationsController POST #create when invited user IS an existing user correctly finds User record of invited user
Failure/Error: expect {
You must pass an argument rather than a block to use the provided matcher (include "valentin#parisian.org"), or the matcher must implement `supports_block_expectations?`.
# ./spec/controllers/users/invitations_controller_spec.rb:17:in `block (4 levels) in <top (required)>'
I am not surprised by the error, because the Test doesn't feel right to me. I just can't quite figure out how to test for this without writing code in my controller#action.
I am using FactoryGirl and it works perfectly, in the sense that it returns valid data for all the data-types. The issue here is how do I get RSpec to actually test for the functionality I need.
The error you are getting is a syntax error, nothing related to whatever your action is supposed to do.
The code you have there it is being interpreted as you are passing a block ({}) to the expect method.
I'd change it to something like
it 'correctly finds User record of invited user' do
post :create, { email: #users.first[:email] }
expect(response).to include(#users.first[:email])
end
Assuming that the response of the create action returns the email as plain text, which seems weird to me.
Also note that I have email directly passed to the post since you mentioned you were expecting it in params[:email] but by the test you wrote seems like you were expecting it in params[:invitation][:email].
Change that part if that is the case.
OK first, I should say while I've read a lot about should_receive, I'm still not entirely sure I'm understanding the concept behind it, so what I'm doing could potentially be completely not possible.
I have the following:
class PlansController
def destroy
plan = plan.find_by_id(params[:id])
if plan.cancel_stripe_subscription(params[:reason])
flash[:success] = "success"
redirect_to root_path
else
#error handling
end
end
end
class Plan
def cancel_stripe_subscription(reason)
self.status = "canceled"
self.cancellation_reason = reason
if self.save
return true
else
return false
end
end
In my controller spec, I am thinking it makes sense to do a test that the cancel_stripe_subscription method is called successfully (using 1should_receive1), with the right arguments and everything, and another test that the output of the destroy action is correct.
In other words, I thought to write the following controller spec:
describe PlansController, "Destroy Action" do
before do
#plan = Plan.create(...)
end
it "should have called destroy action" do
delete :destroy,
plan: {
id: #plan.id,
reason: "something"
}
assigns(:plan).should_receive(:cancel_stripe_subscription).with(reason:"something").exactly(1).times.and_return(true)
end
it "should have called destroy action" do
delete :destroy,
plan: {
id: #plan.id,
reason: "something"
}
assigns(:plan).status.should == "canceled"
assigns(:plan).cancellation_reason.should == "something"
end
end
The second test passes, but the first throws
Failure/Error: assigns(:plan).should_receive(:cancel_stripe_subscription)
(#<Plan:0x007fe282931310>).cancel_stripe_subscription(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
So I really have two questions:
Just to confirm, am I using should_receive correctly? Should I even be testing for this? Or is the second test generally accepted as enough?
If I should be testing for this, what's the right way of using should_receive? (Note, have not had luck with expect(#plan).to have_received(:cancel_stripe_subscription) either)
I think the confusion here is in part due to combining two different styles of testing, mockist (your first test) and classicist (your second test). It's fine to use one or the other, based on your preferred testing style, but using both together to test the same piece of code is somewhat redundant.
A should_receive expectation has to be set before you call the method under test; you're setting it afterwards. Since you need to set it before, you then have to make sure the object you've set up the expectation on ends up being operated on in the action. The normal way would be to stub out find_by_id on Plan, like this:
it "should have called destroy action" do
Plan.stub(:find_by_id).and_return(#plan)
assigns(:plan).should_receive(:cancel_stripe_subscription).with(reason:"something").exactly(1).times.and_return(true)
delete :destroy, plan: { id: #plan.id, reason: "something" }
end
(I am assuming that you meant to write plan = Plan.find_by_id(params[:id]) in the first line of your destroy action.)
As to whether you should be testing it this way, I'd say that your second test does a good enough job of verifying the outcome that you want, and you don't really need to go to all the trouble.
When writing tests using RSpec, I regularly have the need to express something like
Klass.any_instance_with_id(id).expects(:method)
Main reason is that in my test, I often have the object that should receive that method call available, but due to the fact that ActiveRecord, when loading the object with that id from the database, will create a different instance, I can't put the "expects" on my own instance
Sometimes I can stub the find method to force ActiveRecord to load my instance, sometimes I can stub other methods, but having that "any_instance_with_id" would make life so much easier...
Can't image I'm the first having this problem... So if any of you found a "workaround", I'd be glad to find out!
Example illustrating the need:
controller spec:
describe 'an authorized email' do
let(:lead) { create(:lead, status: 'approved') }
it "should invoice its organisation in case the organisation exceeds its credit limit" do
lead.organisation.expects :invoice_leads
get :email
end
end
controller:
def email
leads = Lead.approved
leads.each do |lead|
lead.organisation.invoice_leads if lead.organisation.credit_limit_exceeded?
end
redirect_to root_path
end
It seems weird to me you need that for specs.
You should take the problem one level higher: when your app tries to retrieve the record.
Example:
#code
#user = User.find(session[:user_id])
# spec
let(:fake_user) { mock_model 'User', method: false }
it 'description' do
User.should_receive(:find).and_return fake_user
fake_user.expects(:method)
#...
end
Order/invoice example:
let(:order) { mock_model 'order', invoice: invoice }
let(:invoice) { mock_model 'Invoice', 'archive!' => false }