How best to DRY up this Rspec code? - ruby-on-rails

I have the following Rspec code which I think can be DRY'ed up quite a bit, specifically the check_creation_email_sent and check_rename_email_sent methods, I just don't know how to do it.
I tried doing it with shared_examples and it_behaves_like but got the error undefined method it_behaves_like... so I'm hoping someone can shed some light for me.
require 'spec_helper'
describe CreateTopicFolder do
let(:ids) { [123,456] }
def clear_bucket(topic_id)
subject.find_folder(topic_id).each { |obj| obj.delete } unless subject.find_folder(topic_id).nil?
end
def check_folder_and_object(topic_id, permalink, old_permalink=nil)
folder = subject.find_folder(topic_id)
folder.should_not be_empty
folder.size.should == 1
object = folder.first
object.key.should include(permalink)
object.key.should_not include(old_permalink) unless old_permalink.nil?
end
def check_creation_email_sent(email, permalink)
ActionMailer::Base.deliveries.should_not be_empty
email.subject.should match("Folder for '#{permalink}' created")
email.body.should include("(You can reply to this e-mail to contact Engineering if something doesn't look right)")
end
def check_rename_email_sent(email, permalink, old_permalink)
ActionMailer::Base.deliveries.should_not be_empty
email.subject.should match("Folder for '#{old_permalink}' renamed to '#{permalink}'")
email.body.should include("(You can reply to this e-mail to contact Engineering if something doesn't look right)")
end
before(:all) do
ids.each { |folder| clear_bucket(folder) }
permalink = "123-How-to-Have-Fun"
subject.create_or_rename_folder(permalink)
end
it 'should find an existing folder in the S3 bucket if it exists' do
subject.find_folder(123).should_not be_empty
end
it 'should create a new folder with the topic permalink as name if none already exists' do
subject.find_folder(456).should be_nil
permalink = "456-How-to-Not-Have-Any-Fun"
subject.create_or_rename_folder(permalink)
email = subject.instance_variable_get(:#email)
check_creation_email_sent(email, permalink)
check_folder_and_object(456, permalink)
end
it 'should rename an existing folder with new permalink' do
subject.find_folder(456).should_not be_empty
permalink = "456-How-to-Have-Fun-Again"
old_permalink = subject.instance_variable_get(:#old_permalink)
subject.create_or_rename_folder(permalink)
email = subject.instance_variable_get(:#email)
check_rename_email_sent(email, permalink, old_permalink)
check_folder_and_object(456, permalink, old_permalink)
end
end
Thanks in advance for any help!

You're probably looking for shared_examples_for("a thing") and it_should_behave_like("a thing").

Related

Rspec - Testing method received call inside another method

In my Rails application I am testing a instance method that sends a SMS message out. It looks like this:
group.rb
def send_notifications
twilio_sms = Twilio::REST::Client.new('mykey', 'mykey')
begin
twilio_sms.account.messages.create(
from: "+188888888",
to: "+1#{phone_number}",
body: "Message from Company: New item uploaded to group"
)
rescue
puts 'Invalid number'
end
end
I'm having trouble figuring out how to test the twilio_sms.account.messages.create part.
My spec looks something like this so far:
group_spec.rb
describe Group do
context 'instance methods' do
describe '.send_notifictions' do
it 'calls the create method on the twilio messages object' do
twilio_messages = instance_double("Twilio::REST::Messages")
expect(twilio_messages).to receive(:create)
group = create(:group_with_notifications)
group.send_notifications
end
end
end
end
This obviously isn't working or I wouldn't be asking this question. What is the proper way to test that the create message was sent properly? Or am I approaching this the wrong way? Any help would be appreciated, thanks!
Try this
describe Group do
describe 'instance methods' do
describe '.send_notifictions' do
it 'creates a new message' do
twilio_sms = double(:twilio_sms)
twilio_messages = double(:twilio_messages)
expect(Twilio::REST::Client).to receive(:new).with('mykey', 'mykey').and_return(twilio_sms)
expect(twilio_sms).to receive(:account).and_return(twilio_messages)
expect(twilio_messages).to receive(:messages).and_return(twilio_messages)
expect(twilio_messages).to receive(:create).with({:from => "+188888888", :to => "+15555555", :body => "Message from Company: New item uploaded to group"}).and_return(true)
group = Group.create(some_attributes: 'foo')
group.send_notifications
end
end
end
end

Reset Password testing with Rspec

i am using rails and want to write a test for password reset in Rspec. i am quite new to testing.
this is what i have done so far:
require 'rails_helper'
describe UsersController, type: :controller do
describe 'post #reset_password' do
let(:user) { create(:user) }
context "reset password" do
def do_request
patch :update_password
end
before { do_request }
it { expect(ActionMailer::Base.deliveries.count(1) }
end
end
end
every time i run this it gives ma an syntax error in
"it { expect(ActionMailer::Base.deliveries.count(1) } ".
i want to check whether the email successfully sent of not and if the user have key in the email.
Thanks!
1) you miss ) at last here so got syntax error
it { expect(ActionMailer::Base.deliveries.count(1) }
to
it { expect(ActionMailer::Base.deliveries.count(1)) }
2)
If you want to check total deliveries. you can try
it 'should send an email' do
ActionMailer::Base.deliveries.count.should == 1
end
also check sender
it 'renders the sender email' do
ActionMailer::Base.deliveries.first.from.should == ['notifications#domain.com']
end
Also check subject line
it 'should set the subject to the correct subject' do
ActionMailer::Base.deliveries.first.subject.should == 'Here Is Your Story!'
end
The problems you're having will most likely be fixed by writing better tests.
Here's generally how you would write tests for something like this.
Lets suppose in your routes file you have a post route that looks something like this
# config/routes.rb
post "/user/:id/reset_password", to: "users#reset_password"
And your User controller looks something like this
# app/controllers/users_controller.rb
class UsersController
...
def reset_password
user = User.find(params[:id])
user.reset_password!
SomeMailClass.email_reset_instructions(user)
end
end
and your User.rb model looks something like this
# app/models/user.rb
class User < ActiveRecord::Base
def reset_password!
update!(password: nil) # or whatever way you want/need to reset the password
end
end
and you have some type of mailing class to send your email
# app/models/some_mail_class.rb
class SomeMailClass
def self.email_reset_instructions(user)
# do something to send email...
end
end
The way you would go about testing this in the controller would be
# spec/controllers/users_controller_spec.rb
require 'rails_helper'
describe UsersController, type: :controller do
it "#reset_password" do
user_id = double(:user_id)
user = double(:user)
expect(User).to receive(:find).with(user_id).and_return(user)
expect(user).to receive(:reset_password!).and_return(true)
expect(SomeMailClass).to receive(:email_reset_instructions).with(user)
post :reset_password, id: user_id
end
end
But you shouldn't stop there. Because the implementation of the newly made method reset_password! and the SomeMailClass has yet to be tested. So you would write model/unit tests like this for them
# spec/models/user_spec.rb
require "rails_helper"
describe User do
it ".reset_password!" do
user = User.create(password: "foo")
expect(user.password).to eq "foo"
user.reset_password!
expect(user.password).to eq nil
end
end
Then you might install vcr and factory_girl gems and use them like so to test your mailer
# spec/models/some_mail_class_spec.rb
require "rails_helper"
describe SomeMailClass do
VCR.use_cassette "email_reset_instructions" do |cassette|
it ".email_reset_instructions" do
user = FactoryGirl.create(:user)
SomeMailClass.email_reset_instructions(user)
# you can write some expectations on the cassette obj to test.
# or you can write whatever expectations you need/desire
end
end
end
And in the end if there was something happening on the front end that a user would click that made this post request you would write a feature test for it as well.
Hope this helps!

Reassign user with nil id

I ran into a surprising Rails/Rspec/FactoryGirl (?) quirk today. I've since found a solution, but I'm interested in why the problem happened in the first place.
My example:
let(:old_user) { build(:user) }
let(:new_user) { create(:user) }
let(:post) { build(:post, user: old_user) }
it 'sets the new user' do
post.user_id = new_user.id
post.save
post.reload
post.user.should == new_user
end
This test fails. The user is not assigned properly, at the end of the test post.user is still old_user. (It also is a completely useless test.)
There are a couple of ways to get the test to pass. One is to create the old_user, so that post.user_id is set before trying to reassign it. Another is to replace post.user_id = new_user.id with post.user = User.find(new_user.id). But I'm curious...
What's happening behind the scenes with the original code when the test fails?
Edit: Adding in the factories as requested.
FactoryGirl.define do
factory :user do
# We actually use Faker, but for this purpose I gave strings
name 'Test User'
email 'example#example.com'
password 'changeme'
password_confirmation 'changeme'
end
end
# The Post class has `belongs_to :user`
FactoryGirl.define do
factory :post do
body 'I am a post'
end
end

Rails response.should be_success is never true

I am following Michael Hartl's excellent tutorial on Ruby on Rails. I'm stuck trying to understand the way ActionDispatch::Response works. This derives from Exercise 9 of Chapter 9 (Rails version 3.2.3).
In particular we're asked to make sure that the admin user is unable to User#destroy himself. I have an idea how to do that, but since I'm trying to follow a TDD methodology, I'm first writing the tests.
This is the relevant snippet in my test:
describe "authorization" do
describe "as non-admin user" do
let(:admin) {FactoryGirl.create(:admin)}
let(:non_admin) {FactoryGirl.create(:user)}
before{valid_signin non_admin}
describe "submitting a DELETE request to the Users#destroy action" do
before do
delete user_path(admin)
#puts response.message
puts response.succes?
end
specify{ response.should redirect_to(root_path) }
specify{ response.should_not be_success }
end
end
#Exercise 9.6-9 prevent admin from destroying himself
describe "as admin user" do
let(:admin){FactoryGirl.create(:admin)}
let(:non_admin){FactoryGirl.create(:user)}
before do
valid_signin admin
end
it "should be able to delete another user" do
expect { delete user_path(non_admin) }.to change(User, :count).by(-1)
end
describe "can destroy others" do
before do
puts admin.admin?
delete user_path(non_admin)
puts response.success?
end
#specify{response.should be_success}
specify{response.should_not be_redirect}
end
describe "cannot destroy himself" do
before do
delete user_path(admin)
puts response.success?
end
#specify{response.should_not be_success}
specify{response.should be_redirect}
end
end
.
.
.
end
All the tests pass except the "can destroy others" test.
However, if I puts response.success? after every delete request, I always get False, so none of the requests "succeed".
Manually interacting with the webapp and deleting users works just fine, so I assume that response.success does not mean that the detroy(or whatever request for that matter) was not successful, but something else. I read it has to do with the difference between HTTP responses 200/302/400, but I'm not totally sure.
For the record, this is my User#destroy:
def destroy
User.find(params[:id]).destroy
flash[:success]="User destroyed."
redirect_to users_path
end
Any light on this?
thanks!
Edit
This is my factory:
FactoryGirl.define do
factory :user do
sequence(:name){ |n| "Person #{n}" }
sequence(:email){ |n| "person_#{n}#example.com"}
password "foobar"
password_confirmation "foobar"
factory :admin do
admin true
end
end
end
Edit 2 as suggested by #Peter Alfvin, I changed lines
let(:user){FactoryGirl.create(:user)}
to
let(:admin){FactoryGirl.create(:admin)}
And all user to admin in general. I also added a puts admin.admin? before the delete request. Still not working!
Edit 3
Changing the test "can destroy others" as:
describe "can destroy others" do
before do
puts admin.admin?
delete user_path(non_admin)
puts response.success?
end
#specify{response.should be_success}
specify{response.should_not be_redirect}
end
Does not seem to help either.
For your "admin" case, you're still creating and logging in as a "regular" user instead of an admin user, which is why you can't destroy anyone else.
response.success does indeed refer to the HTTP response code. By default, I believe this is anything in the 200 range. redirect_to is in the 300 range.
Make sure your user Factory includes this line
factory :user do
#your user factory code
factory :admin do
admin true
end
end
Then FactoryGirl.create(:admin) will return an admin user or you can also use user.toggle!(:admin) which will switch a standard user to an admin user.
try this then
describe "as admin user" do
let(:admin){FactoryGirl.create(:admin)}
let(:non_admin){FactoryGirl.create(:user)}
before do
valid_signin admin
end
it "should be able to delete another user" do
expect { delete user_path(non_admin) }.to change(User, :count).by(-1)
end
it "can destroy others" do #
before do
puts admin.admin?
delete user_path(non_admin)
puts response.success?
end
#specify{response.should be_success}
specify{response.should_not be_redirect}
end
it "cannot destroy himself" do
before do
delete user_path(admin)
puts response.success?
end
#specify{response.should_not be_success}
specify{response.should be_redirect}
end
end
describe creates a magic Class it becomes a subClass of the describe class from my understanding. Rails has a lot of this magic and it can get confusing. Also I have not seen your controller but what are you expecting to happen when you destroy a user because if you followed the tutorial then there will be a redirect delete sent through the browser will call your destroy method in the UsersController which in the tutorial has this line redirect_to users_url so response.should_not be_redirect will always fail because the spec is wrong not the controller.

Why doesn't this RSpec stub work?

i have created an rspec test like :
it "should redirect to '/tavern' with an error if user already has a tavern quest" do
user = mock('User')
user.stub(:has_tavern_quest).and_return(true)
post :new_quest, :quest_type => 3
flash[:error].should_not be_nil
response.should redirect_to tavern_path
end
Then, i wrote the controller part :
# check if user already has a tavern quest
if current_user.has_tavern_quest?
flash[:error] = 'You already have a quest to finish !'
redirect_to tavern_path and return
end
And the model part :
def has_tavern_quest?
TavernQuest.exists?(self.id)
end
I would expect that the test succeeds, now but i get :
1) TavernController POST '/quest/' to get a new quest of quest_type == 3 should redirect to '/tavern' with an error if user already has a tavern quest
Failure/Error: flash[:error].should_not be_nil
expected: not nil
got: nil
# ./spec/controllers/tavern_controller_spec.rb:29
Do i have a mistake somewhere ?
THE MACRO FOR LOGIN USER :
module ControllerMacros
def login_user
before(:each) do
#request.env["devise.mapping"] = :user
#user = Factory.create(:user)
sign_in #user
end
end
end
Untested:
it "should redirect to '/tavern' with an error if user already has a tavern quest" do
controller.stub_chain(:current_user,:has_tavern_quest?).and_return(true)
post :new_quest, :quest_type => 3
flash[:error].should_not be_nil
response.should redirect_to tavern_path
end
Your mock doesn't do anything... perhaps you meant to use it somewhere?
I personally dislike mocking in this case and feel it's obfuscation. If you are using Devise you could use their test helpers to sign in as a user.

Resources