Nested RSpec Tests - ruby

I am following Michael Hartl's tutorial and came across this following codes that I am having trouble comprehending.
describe "index" do
let(:user) { FactoryGirl.create(:user) }
before(:each) do
sign_in user
visit users_path
end
it { should have_title('All users') }
it { should have_content('All users') }
describe "pagination" do
before(:all) { 30.times { FactoryGirl.create(:user) } }
after(:all) { User.delete_all }
it { should have_selector('div.pagination') }
it "should list each user" do
User.paginate(page: 1).each do |user|
expect(page).to have_selector('li', text: user.name)
end
end
end
end
My question is:
is this a NESTED TEST where the test block of Pagination runs inside Index Test block? in other words, the sequence of testing flow:
before(:each) outer block of signing in user and visiting user path is executed
then the inner block of 30.times { FactoryGirl.create(:user) is executed
then the inner block of it { should have_selector('div.pagination') } is executed
then the inner block of expect(page).to have_selector('li', text: user.name) is executed
thank you

Here's the flow for the above test:
The before(:each) block is executed before each of the following:
it { should have_title('All users') }
it { should have_content('All users') }
Then, the before(:each) is executed again, followed by the describe block, which executes:
before(:all) { 30.times { FactoryGirl.create(:user) } }
it { should have_selector('div.pagination') }
it "should list each user" do
User.paginate(page: 1).each do |user|
expect(page).to have_selector('li', text: user.name)
end
end
Finally, after(:all) { User.delete_all } is executed.
I hope this helps explain the flow.

Related

overwrite let inside an it method to alter params send to subject

It's possible to overwrite a let value inside an spec? I wanted to be able to set subject and modify my params within each test, something like:
subject {
MyClass.new params
}
let(:params) { {} }
describe '#initialize' do
it 'should set new params' do
params = {a: 1}
expect{ subject }.to do_something
end
it 'should raise with string' do
params = 'string'
expect{ subject }.to raise_error
end
end
or what is the correct way to approach this? should I wrote my expect{} with the whole class name?
I would rewrite the specs like this:
subject { -> { MyClass.new(params) } } # Note: subject is a lambda
describe '#initialize' do
context 'with blank params' do
let(:params) { {} }
it { is_expected.to do_something }
end
context 'with string params' do
let(:params) { 'string' }
it { is_expected.to raise_error }
end
end
But - as max pleaner already said - in this simple example it would probably be more readable and maintainable to skip the subject and just use the MyClass.new ... call directly in the expectation

FactoryGirl Sequence not instantiating when creating an object with association

I have to write a few tests for a controller I have. I have 3 models : User, UserInfo and UserPaymentPreference. I am using FactoryGirl to make a UserPaymentPreference object like this:
factory :advertiser_payment_preference_sequence do
association :advertiser_user, factory: [:advertiser_user_sequence]
end
factory :advertiser_user_sequence, class: 'AdvertiserUser' do
deposit { rand(100..1000).round(2) }
sequence(:login) { |n| "#{Faker::Internet.domain_name}#{n}" }
user_type 'advertiser'
association :user_info, factory: :advertiser_user_info_sequence
trait :no_deposit do
deposit 0
end
end
factory :advertiser_user_info_sequence, class: 'AdvertiserUserInfo' do
sequence(:firstname) { |n| "#{Faker::Name.first_name}#{n}" }
sequence(:organization) { |n| "#{Faker::Company.name}#{n}" }
sequence(:phone) { |n| "#{Faker::PhoneNumber.cell_phone}#{n}" }
sequence(:fiscal_code) { |n| "#{Faker::Company.duns_number}#{n}" }
sequence(:address) { |n| "#{Faker::Address.street_address}#{n}" }
sequence(:city) { |n| "#{Faker::Address.city}#{n}" }
end
This works fine if I do something like FactoryGirl.build(:advertiser_user_info_sequence) it will create an object of type UserInfo with all the fields populated as they should be.
However when I try to make an AdvertiserPaymentPreference object by doing FactoryGirl.build(:advertiser_payment_preference_sequence) all the user_info fields will be "" instead of the value that it should have been created by the association.
describe 'Advertiser::Billing::SettingsController', type: :request do
let!(:campaign) { create :campaign }
let!(:user) { campaign.user }
let!(:user_info) { user.user_info }
let!(:advertiser_payment_preference) { create(:advertiser_payment_preference, advertiser_user_id: user.id)}
before :each do
login(user)
end
describe "PUT/PATCH /advertiser/billing/settings" do
context "it is a valid request" do
it "updates the resource" do
binding.pry
new_advertiser_payment_preference = build(:advertiser_payment_preference_sequence)
binding.pry
params = {}
params['user_info'] = to_api_advertiser_info(new_advertiser_user_info)
api_put '/advertiser/billing/profile', params
expect(response.status).to eq(200)
user_info.reload
expect(user_info.organization).to eq(new_advertiser_user_info.organization)
expect(user_info.phone).to eq(new_advertiser_user_info.phone)
expect(user_info.fiscal_code).to eq(new_advertiser_user_info.fiscal_code)
expect(user_info.country).to eq(new_advertiser_user_info.country)
expect(user_info.address).to eq(new_advertiser_user_info.address)
end
end
end
end
This is how the test for the controller looks like . When I call new_advertiser_payment_preference = build(:advertiser_payment_preference_sequence) I get an object user_info with "" on all fields that are made using advertser_user_info_sequence.

RSpec argument match array of hashes

I would like to use argument matchers provided by rspec to match an array of hashes. This is ideally the code I would like:
context 'logging stock levels' do
subject { double(:stock_logger, stock_updated: nil) }
let(:stock_importer) { described_class.new(logger: subject) }
before(:each) { stock_importer.import }
it { is_expected.to have_received(:stock_updated)
.with(array_including(hash_including('sku', 'count_on_hand'))) }
end
This errorred with an argument mismatch for me. The only working solution I can come up with is the following:
context 'logging stock levels' do
subject { double(:stock_logger, stock_updated: nil) }
let(:stock_importer) { described_class.new(logger: subject) }
before(:each) { stock_importer.import }
it do
is_expected.to have_received(:stock_updated) do |stock_levels|
expect(stock_levels).to include(include('sku', 'count_on_hand'))
end
end
end
Was I just doing something wrong?
With Rspec 3.3, This worked for me:
expect(<Object>).to receive(:method).with(
array_including(hash_including( {"id" => some_id} ))
)
Try array_including but with the hash keys. Untested, but perhaps something like:
it do
is_expected.to have_received(:stock_updated)
.with(include(include("sku"), include("bar")))
end

Hartl Chap. 9.2.2 test fail

I am about to proceed to sec. 9.2.3 when all of the sudden my spec test turns to RED!!!!
what am i doing wrong?? Here are my errors:
Failures:
1) Authentication authorization as wrong user visiting Users#edit page
Failure/Error: it { should_not have_selector('title', text: full_title('Edit user')) }
expected css "title" with text "Ruby on Rails Tutorial Sample App | Edit user" not to return anything
# ./spec/requests/authentication_pages_spec.rb:72:in `block (5 levels) in <top (required)>'
2) Authentication authorization as wrong user submitting a PUT request to the Users#update action
Failure/Error: specify { response.should redirect_to(root_path) }
Expected response to be a <:redirect>, but was <200>
# ./spec/requests/authentication_pages_spec.rb:77:in `block (5 levels) in <top (required)>'
Finished in 1.57 seconds
64 examples, 2 failures
Failed examples:
rspec ./spec/requests/authentication_pages_spec.rb:72 # Authentication authorization as wrong user visiting Users#edit page
rspec ./spec/requests/authentication_pages_spec.rb:77 # Authentication authorization as wrong user submitting a PUT request to the Users#update action
authentication_pages_spec.rb
require 'spec_helper'
describe "Authentication" do
subject { page }
describe "signin page" do
before { visit signin_path }
it { should have_selector('h1', text: 'Sign in') }
it { should have_selector('title', text: 'Sign in') }
end
describe "signin" do
before { visit signin_path }
describe "with invalid information" do
before { click_button "Sign in" }
it { should have_selector('title', text: 'Sign in') }
it { should have_selector('div.alert.alert-error', text: 'Invalid') }
describe "after visiting another page" do
before { click_link "Home" }
it { should_not have_selector('div.alert.alert-error') }
end
end
describe "with valid information" do
let(:user) { FactoryGirl.create(:user) }
before { sign_in user }
it { should have_selector('title', text: user.name) }
it { should have_link('Profile', href: user_path(user)) }
it { should have_link('Settings', href: edit_user_path(user)) }
it { should have_link('Sign out', href: signout_path) }
it { should_not have_link('Sign in', href: signin_path) }
describe "followed by signout" do
before { click_link "Sign out" }
it { should have_link('Sign in') }
end
end
end
describe "authorization" do
describe "for non-signed-in users" do
let(:user) { FactoryGirl.create(:user) }
describe "in the Users controller" do
describe "visiting the edit page" do
before { visit edit_user_path(user) }
it { should have_selector('title', text: 'Sign in') }
end
describe "submitting to the update action" do
before { put user_path(user) }
specify { response.should redirect_to(signin_path) }
end
end
end
describe "as wrong user" do
let(:user) { FactoryGirl.create(:user) }
let(:wrong_user) { FactoryGirl.create(:user, email: "wrong#example.com") }
before { sign_in user }
describe "visiting Users#edit page" do
before { visit edit_user_path(wrong_user) }
it { should_not have_selector('title', text: full_title('Edit user')) }
end
describe "submitting a PUT request to the Users#update action" do
before { put user_path(wrong_user) }
specify { response.should redirect_to(root_path) }
end
end
end
end
users_controller.rb
class UsersController < ApplicationController
before_filter :signed_in_user, only: [:edit, :update]
def show
#user = User.find(params[:id])
end
def new
#user = User.new
end
def create
#user = User.new(params[:user])
if #user.save
sign_in #user
flash[:success] = "Welcome to the Sample App!"
redirect_to #user
else
render 'new'
end
end
def edit
#user = User.find(params[:id])
end
def update
#user = User.find(params[:id])
if #user.update_attributes(params[:user])
flash[:success] = "Profile updated"
sign_in #user
redirect_to #user
else
render 'edit'
end
end
private
def signed_in_user
redirect_to signin_url, notice: "Please sign in." unless signed_in?
end
def correct_user
#user = User.find(params[:id])
redirect_to(root_path) unless current_user?(#user)
end
end
sessions_helper.rb
module SessionsHelper
def sign_in(user)
cookies.permanent[:remember_token] = user.remember_token
self.current_user = user
end
def signed_in?
!current_user.nil?
end
def current_user=(user)
#current_user = user
end
def current_user
#current_user ||= User.find_by_remember_token(cookies[:remember_token])
end
def current_user?(user)
user == current_user
end
def sign_out
self.current_user = nil
cookies.delete(:remember_token)
end
end
user_pages_spec.rb
require 'spec_helper'
describe "User pages" do
subject { page }
describe "profile page" do
let(:user) { FactoryGirl.create(:user) }
before { visit user_path(user) }
it { should have_selector('h1', text: user.name) }
it { should have_selector('title', text: user.name) }
end
describe "signup page" do
before { visit signup_path }
it { should have_selector('h1', text: 'Sign up') }
it { should have_selector('title', text: full_title('Sign up')) }
end
describe "signup" do
before { visit signup_path }
let(:submit) { "Create my account" }
describe "with invalid information" do
it "should not create a user" do
expect { click_button submit }.not_to change(User, :count)
end
end
describe "with valid information" do
before do
fill_in "Name", with: "Example User"
fill_in "Email", with: "user#example.com"
fill_in "Password", with: "foobar"
fill_in "Confirmation", with: "foobar"
end
describe "after saving the user" do
before { click_button submit }
let(:user) { User.find_by_email('user#example.com') }
it { should have_selector('title', text: user.name) }
it { should have_selector('div.alert.alert-success', text: 'Welcome') }
it { should have_link('Sign out') }
end
it "should create a user" do
expect { click_button submit }.to change(User, :count).by(1)
end
end
end
describe "edit" do
let(:user) { FactoryGirl.create(:user) }
before do
sign_in user
visit edit_user_path(user)
end
describe "page" do
it { should have_selector('h1', text: "Update your profile") }
it { should have_selector('title', text: "Edit user") }
it { should have_link('change', href: 'http://gravatar.com/emails') }
end
describe "with invalid information" do
before { click_button "Save changes" }
it { should have_content('error') }
end
describe "with valid information" do
let(:new_name) { "New Name" }
let(:new_email) { "new#example.com" }
before do
fill_in "Name", with: new_name
fill_in "Email", with: new_email
fill_in "Password", with: user.password
fill_in "Confirm Password", with: user.password
click_button "Save changes"
end
it { should have_selector('title', text: new_name) }
it { should have_selector('div.alert.alert-success') }
it { should have_link('Sign out', href: signout_path) }
specify { user.reload.name.should == new_name }
specify { user.reload.email.should == new_email }
end
end
end
What could be the issue? The stuff is pretty much exactly what the tutorial calls for.
Change your edit action to:
def edit
#user = User.find(params[:id])
end
And the first line in your update action should also do this:
def update
#user = User.find(params[:id])
...
For others who might run into this, UsersController is missing the following:
before_action :correct_user, only: [:edit, :update]
This uses your method in the SessionsHelper and since it's done before edit and update #user = User.find(params[:id]) in edit and update the same evaluation becomes superfluous.

How to refactor RSpec tests for API

I've got a series of RSpec tests for a Sinatra based API, and would like to refactor them to make them a little simpler and reduce repetition.
Here's an example a test for a route:
describe 'post /sections with empty data' do
before do
params = {
:site_id => site.id,
:page_id => page.id,
}
post '/sections', params, #session
end
specify { last_response.status.should == 200 }
specify { json_response['id'].should_not be_nil }
specify { json_response['type'].should == default_section_type }
end
Each test will be using the same base URL, with the same session data, the only difference is the parameters, and what the responses should be. There's at least 4 tests (GET, POST, PUT, DELETE) per route, and usually more.
Is there a way of making these tests more manageable?
Without resorting to metaprogramimng, you can use nested describe blocks to override only the parameters you want:
describe "/sessions" do
before do
send(http_method, "/sessions", params, #session)
end
describe "with POST" do
let(:http_method) { :post }
describe "and empty data" do
let(:params) do
{ :site_id => site.id, :page_id => page.id }
end
specify { last_response.status.should == 200 }
specify { json_response['id'].should_not be_nil }
specify { json_response['type'].should == default_section_type }
end
describe "with non-empty data" do
let(:params) do
# relevant params
end
end
end
describe "with GET" do
let(:http_method) { :get }
# ...
end
end
Have no idea if this works but it can give you an idea of what you can do
describe ' /sections with empty data' do
before(:all) do
#params = {
:site_id => site.id,
:page_id => page.id,
}
end
after(:each) do
specify { last_response.status.should == 200 }
specify { json_response['id'].should_not be_nil }
specify { json_response['type'].should == default_section_type }
end
[:get, :post, :put, :delete].each do |http_method|
it "works with #{http_method}" do
send(http_method) '/sections', #params, #session
end
end
end
Update
Reading your question again made me realize that this is not what you actually asked for. If it doesn't help at all tell me so I delete it.

Resources