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.
Related
I have a rails method which allows a user to submit a review and to counterparty, it sends an email using the delayed jobs.
def update_review
#review.add_review_content(review_params)
ReviewMailer.delay.review_posted(#review.product_owner, params[:id])
end
And I am trying to add a rspec test for this to check if the mailer is delivered properly and to whom. The delayed jobs run immediately after they are created on test because I want other jobs like the job to update the product owners overall rating to be completed immediately.
So the email does get fired but how can I add a test for it?
EDIT: Adding current tests
My current tests are:
describe 'PUT #update the review' do
let(:attr) do
{ rating: 3.0, raw_review: 'Some example review here' }
end
before(:each) do
#review = FactoryBot.create :review
put :update, id: #review.id, review: attr
end
it 'creates a job' do
ActiveJob::Base.queue_adapter = :test
expect {
AdminMailer.review_posted(#coach, #review.id).deliver_later
}.to have_enqueued_job
end
it { should respond_with 200 }
end
This does test that the mailer works properly but I want to test that it gets triggered properly in the method flow as well.
It sounds like what you want is to ensure that the update_review method enqueues a job to send the correct email to the correct recipient. Here's a simpler way to accomplish that:
describe 'PUT #update the review' do
let(:params) { { rating: rating, raw_review: raw_review } }
let(:rating) { 3.0 }
let(:raw_review) { 'Some example review here' }
let(:review) { FactoryBot.create(:review) }
let(:delayed_review_mailer) { instance_double(ReviewMailer) }
before do
# assuming this is how the controller finds the review...
allow(Review).to receive(:find).and_return(review)
# mock the method chain that enqueues the job to send the email
allow(ReviewMailer).to receive(:delay).and_return(delayed_review_mailer)
allow(delayed_review_mailer).to receive(:review_posted)
put :update, id: review.id review: params
end
it 'adds the review content to the review' do
review.reload
expect(review.rating).to eq(rating)
expect(review.raw_review).to eq(raw_review)
end
it 'sends a delayed email' do
expect(ReviewMailer).to have_received(:delay)
end
it 'sends a review posted email to the product owner' do
expect(delayed_review_mailer)
.to have_received(:review_posted)
.with(review.product_owner, review.id)
end
end
The reason I prefer this approach is that a) it could be done without touching the database at all (by swapping the factory for an instance double), and b) it doesn't try to test parts of Rails that were already tested by the folks who built Rails, like ActiveJob and ActionMailer. You can trust Rails' own unit tests for those classes.
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.)
I am writing tests for my application. I have written quite a few tests and they were working fine. However, two of my tests keep failing and I am not sure why that is. The first test is a unsuccessful editing of a form.
The test is given below
test "unsuccessfull editing of the scoreboard" do
log_in_as(#user)
get edit_scoreboard_path(#scoreboard)
assert_template 'scoreboards/edit' (doesn't work)
patch scoreboard_path(#scoreboard), scoreboard: { name_of_scoreboard: "a"* 51,
name_of_organization: "b"*60,
name_of_activity: "c"*60 }
assert_template 'scoreboard/edit' (doesn't work)
end
The error associated with this test is given below.
ScoreboardEditTest#test_unsuccessfull_editing_of_the_scoreboard [/home/ubuntu/workspace/test/integration/scoreboard_edit_test.rb:13]:
expecting <"scoreboards/edit"> but rendering with <[]>
The controller for the following test is also given below.
def update
#scoreboard = Scoreboard.find(params[:id])
if #scoreboard.update_attributes(scoreboard_params)
redirect_to #scoreboard
flash[:success] = "Updated Successfully"
else
render 'edit'
end
end
After you get the edit_scoreboard_path, the edit scoreboard template should show up. I am not exactly sure why it gives me the error. I have the exact same thing with the user model and its working just fine. I think I am missing something in my understanding of it works.
The second test is a valid creation of a scoreboard. The test is given below.
test "valid creation of the scoreboard with passing validations" do
log_in_as(#user)
get new_scoreboard_path
assert_difference 'Scoreboard.count', 1 do
post scoreboards_path, scoreboard: {name_of_scoreboard: "abc",
name_of_organization: "def",
name_of_activity: "ghi" }
end
assert_redirected_to scoreboard_path(#scoreboard) (doesn't work)
assert_equal 'Scoreboard created successfully', flash[:success]
end
It redirecting to the wrong scoreboard id. In the fixtures I have the id set as 1. The error message is given below.
Expected response to be a redirect to <http://www.example.com/scoreboards/1> but was a redirect to <http://www.example.com/scoreboards/941832920>.
I am not sure exactly what this means. As I mentioned, I have the ID set in the fixtures. I even manually set the id to '941832920'. It still gave me an error. Not sure why its doing that.
For your 2nd test, I don't think you can set an id, it gets assigned by the database when the record is saved. A better way to check whether you get directed to the correct scoreboard path would be the preferred format: assert_redirected_to scoreboard_path(assigns(:scoreboard)).
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.
I'm trying to test so a user cannot send a message to himself. Currently on my new message view, I have a select box which gives a selection of all the users in the system except for the current_user. Currently I only have a test which does not allow a user to select himself as the recipient from the select box:
it { should_not have_select(:receiver_id, :options => [user.name]) }
However, is this enough of a test? Do I need to test creating a new message, setting the :receiver_id to the current_user's id and check for it? If so, where would I put this spec, in the model or a request?
Edit (added a validation method in the Message Model, but my rspec passes even if I comment out the validate line):
Edit 2 (The test for the errors hash does not pass):
Message.rb:
validate :validate_sender_receiver
def validate_sender_receiver
if self.receiver_id == self.sender_id
errors.add(:receiver_id, "Cannot send message to self")
end
end
messages_spec.rb
describe "sending message to yourself" do
before do
#message = user.sent_messages.new(:receiver_id => user.id)
end
it "should not be valid" do
#message.should_not be_valid
end
it "should set the error hash" do
#message.errors.should include("Cannot send message to self")
end
end
If a user hacks your select and adds himself to the possible values you might end up with a message that you don't want. I don't know what your controller's action looks like but you should test that in the model and your model should reject the message if the receiver is the same as the sender.
I changed:
it "should set the error hash" do
#message.errors.should include("Cannot send message to self")
end
to:
it "should set the error hash" do
#message.errors.should have_key(:receiver_id)
end
And it now works out well, still don't understand why the first method doesn't work? Does the have_key just check to see if there is a key, but not if it's empty?