How to make my RSpec describe blocks run independently of one another? - ruby

On my Dashboard page, I have a Metrics section where I show the number of goals a user has. I do not show this section for a user who has no goals. When a user creates a goal and after a redirect the Metrics section will appear.
In the RSpec test below, when RSpec randomly runs the first describe first, the test passes because it does not find the Metrics section. However when RSpec runs the second describe block first, the first describe block fails because by that time the redirect has happened and the Metrics section has appeared.
How do I ensure that each block runs separately and passes?
describe "Dashboard Pages", :type => :request do
subject { page }
let(:user) { FactoryGirl.create(:user) }
before(:each) do
sign_in user
end
describe "After user signs in - No Goals added yet" do
it { is_expected.to have_title(full_title('Dashboard')) }
it { is_expected.to have_content('Signed in successfully')}
it "should not show the metrics section" do
expect(page).to_not have_css("div#metrics")
end
end
#
#Notice that this runs using the SELENIUM WebDriver
#
describe "After user signs in - Add a new Goal" do
it "should display the correct metrics in the dashboard", js: true do
click_link "Create Goal"
fill_in "Goal Name", :with=> "Goal - 1"
fill_in "Type a short text describing this goal:", :with => "A random goal!"
click_button "Save Goal"
end
end
end

I think your problem is that the request sent by click_button "Save Goal" arrives at the server after that test completes. Capybara's Javascript drivers are asynchronous and don't wait for the commands that they send to the browser to complete.
The usual way to get Capybara to wait is to expect something about the page that will be true when the command you want to wait for is complete. That's a good idea here anyway since the last test doesn't actually expect that the metrics are shown like it says it does. So expect that they are:
it "should display the correct metrics in the dashboard", js: true do
click_link "Create Goal"
fill_in "Goal Name", :with=> "Goal - 1"
fill_in "Type a short text describing this goal:", :with => "A random goal!"
click_button "Save Goal"
expect(page).to have_css("div#metrics")
end
Also, note that current RSpec and Capybara don't allow you to use Capybara in request specs. Unless you're tied to old versions for some other reason, I suggest upgrading to current RSpec and Capybara and converting your request spec to a feature spec.

Related

Mongoid Validation Error "Email Can't be blank" when testing with rspec & Capybara, but fields are not blank

I am running into an error when writing an rSpec Capybara test to mock a user signing up for the website.
It should be noted that, unfortunately, I am writing a test for a codebase that is entirely new to me, so a lot of the code for the main program is unknown to me. If asked for something I will try and dig up the relevant code, but I'm not certain what else to include at the moment. However I can say it has been successfully running in production for a while, and I can manually test it successfully - so I think the error is probably in my test, or perhaps some configuration used just while testing.
rspec test
RSpec.describe 'New User Sign Up', type: :feature do
scenario 'valid signup inputs' do
visit("/users/sign_up")
fill_in 'user_name', with: 'TEST'
fill_in 'user_email', with: 'TEST#test.com'
fill_in 'user_username_with_caps', with: "TEST"
fill_in 'user_password', with: 'TESTpw123'
puts find_field('user_email').value
puts expect(find_field('user_email').value).to eq 'TEST#test.com'
puts expect(page).to have_selector("input[value='TEST#test.com']")
puts page.should have_field('user_email', with: 'TEST#test.com')
click_on 'Create an account'
end
end
The output is:
bgc#jadzia:~/Documents/Work/BadgeList/code/badgelist/backend/spec$ bundle exec rspec sign_up_spec.rb
TEST#test.com
true
true
true
F
Failures:
1) New User Sign Up valid signup inputs
Failure/Error: click_on 'Create an account'
Mongoid::Errors::Validations:
message:
Validation of User failed.
summary:
The following errors were found: Email can't be blank
resolution:
Try persisting the document with valid data or remove the validations.
Note the puts statements and the corresponding output before the error message.
As far as I can tell, the fields ARE getting filled in properly. However, somehow this does not get recognized when it attempts to complete the sign up. The error is coming from Mongoid, so somehow mongo reacts differently to an testing auto-entered field vs. a manually entered one.
It should also be noted that, if I disable database_cleaner-mongoid, and run the same test twice... I get a -DIFFERENT- outcome. The test technically passes, but there is a warning prompt on the page that says "This email is taken".
So, somehow the value in the email field is...
Being entered/read properly when directly querying the field value on the page
Not recognized when it immediately afterwards tries to use that value to Create an account, instead the field is seen as blank.
But ALSO the field is successfully saved into the DB for a new account with this information, so running the same test again creates a conflict with the entry from the previous test if the DB is not cleaned first.
All click_on does is click on the button. It doesn't wait for the server to do anything, or anything on the page to change, etc. So ending the test with click_on isn't actually testing for any behavior, and the DB is going to get reset while the action triggered by click_on is still occurring, which is unlikely to be what you want. You need to add an expectation after the click_on testing for what you expect to see in the page like
...
click_on 'Create an account'
expect(page).to have_text('User created!!!)
Also note that the expectations you are calling puts on aren't actually defined to return anything specific, so the fact you're seeing true really is just luck. They're defined to not raise an error when successful, and raise an error when not.

Including shared example from rspec inside of cucumber

The end goal is to include the shared_examples.rb which is included in rails_helper.rb. shared_examples.rb is a copy of this file
https://github.com/tinfoil/devise-two-factor/blob/master/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb
I want to reference the shared_examples.rb in my cucumber test so I can use the method it_behaves_like 'two_factor_authenticatable'
I have the following folder structure:
Updated to include #morissetcl suggested structure
features
step_definitions
sample_step.rb
support
env.rb
sample.feature
spec
models
user_spec.rb
support
shared_examples
shared_example.rb
rails_helper.rb
spec_helper.rb
Both the features and spec folders are at the root of my rails project.
I am trying to include inside the sample_step.rb file the rails_helper.rb which is located in the spec folder.
I tried using different types of require as shown below inside the sample_step.rb file.
require 'spec/spec_helpers/shared_examples'
require '../../spec/spec_helpers/shared_examples'
require_relative '../../spec/spec_helpers/shared_examples'
I keep getting the following error
undefined method `it_behaves_like' for main:Object (NoMethodError)
it_behaves_like is rspec specific. What it does is allow one spec to run another spec for a particular object, so that is not going to work in Cucumber.
What you need to do is have a test suite that has some cukes and some specs. You cover the two factor authentication in detail in rspec, and if you have to always use two factor authentication to login in your cukes you need to write a helper method so you can do that.
To do this I would do the following
Write some step definitions to support login, that delegate the work to a helper method.
Write the helper method
Have the helper method call additional methods to support two factor auth
Add the helper methods to cucumber's world object so they can be called in a step def
So
# features/step_defintions/login
module LoginStepHelper
def login(user: )
login_fill_form_and_submit(user: user)
login_two_factor(user: user)
submit_form
end
def login_fill_form_and_submit(user: )
fill_in :email, user.email
fill_in :password, user.password
end
def login_two_factor(user: )
code = retrieve_2factor_code(user: user)
fill_in :2factor, code
end
...
end
World(LoginStepHeler)
So now you have to work out how the test can get the 2factor code.
Once you have this done you have a tool your step definitions can use to login, so you can write things like
Given 'I am logged in' do
login user: #i
end
Given 'I login as Fred' do
login user: #fred
end
...
note: how many step defs can use your helper method.
You can find more detail about this approach here https://github.com/diabolo/cuke_up which includes details about how to create the test-users that we are passing into the login function in the above code
The solution I came up with was using some of the suggestions that #diabolist made. I looked into the shared examples file: https://github.com/tinfoil/devise-two-factor/blob/master/lib/devise_two_factor/spec_helpers/two_factor_authenticatable_shared_examples.rb
I then used the #validate_and_consume_otp! scenario and setup the data using the info in the before :each block.
The result was as so in my cucumber spec:
When('I fill in the login form with two factor code') do
Timecop.freeze(Time.current)
otp_secret = '2z6hxkdwi3uvrnpn'
#user.otp_secret = otp_secret
otp = ROTP::TOTP.new(otp_secret).at(Time.now)
#user.save # <- This is important to save it so the user has the otp you will pass
Timecop.return
fill_in 'user_email', with: 'user#example.com'
fill_in 'user_password', with: 'password'
fill_in 'user_otp_attempt', with: otp
click_button 'Login'
end
The setup can be cleaned up and put into a method just as #diabolist described so it can be reused by other tests.

Verify a sequence of messages are sent to different objects/classes

I have an object that saves a model and runs a background job.
Class UseCase
...
def self.perform
#account.save
BackgroundJob.perform_later(#account.id)
end
end
In my spec I'd like to test separately that both messages are sent.
I started with something like
it 'saves the account' do
expect_any_instance_of(Account).to receive(:save)
UseCase.perform(account)
end
And this worked fine when I was just saving the account in the perform.
But when I have added the background job the spec doesn't pass anymore since now Couldn't find Account without an ID.
How can I verify (in RSped 3.5) separately that both messages are sent?
UPDATE
it 'runs the job' do
expect(BackgroundJob).to receive(:perform_later).with(instance_of(Fixnum))
UseCase.perform(account)
end
passes so I suppose the account is correctly saved.
However, when I try to inspect #account
def self.perform
#account.save
byebug
BackgroundJob.perform_later(#account.id)
end
In 'saves the account', I get
(byebug) #account
#<Account id: nil, full_name: "john doe" ...>
In 'runs the job', I get
(byebug) #account
#<Account id: 1, full_name: "john doe" ...>
The expectation makes #account a test double so in the first spec the job cannot get the id.
Thanks
The error Couldn't find Account without an ID is actually pretty helpful considering the code that you have inside your perform method.
The issue is mentioned in the comments but I'll elaborate a bit further.
You are using #account.save (I'm assuming #account is an ActiveRecord object) which by definition will return true/false when run (see documentation)
What you probably want is to use save! instead since it will raise a ActiveRecord::RecordInvalid error and stop execution rather than triggering the error you noted earlier. (toss a binding.pry into the method and note what #account is when attempting to call .id)
When you change to save! you can add a test for a case where save might fail (missing attribute, etc). Might look something like this
it 'should raise error when trying to save invalid record' do
# do something to invalidate #account
#account.username = nil
expect { UseCase.perform(#account) }.to raise_error(ActiveRecord::RecordInvalid)
#confirm that no messages were sent
end
Hope this helps you out! GL and let me know if you have any questions / need more help with rspec

How to check if controller response has link?

What's a decent way of doing it? Preferably matching a regexp.
my current code is like this:
describe "get #show" do
context "signed in" do
it "should have a link to edit profile" do
# i sign in the user
get :show
response # i don't know what to do here
end
end
end
Thanks for the help!
It seems like you're trying to do an acceptance test, which probably means you should defer to Capybara. Then you can do magic like
page.should have_css('edit_link', text: 'Edit me')

How to handle cookies when testing with Webrat?

I'm writing Cucumber tests for a Sinatra based application using Webrat. For some tests I need to implement a scenario like
Given I am logged in as admin
When I am visiting "/"
Then I should see "Settings"
I define steps like this:
Given /^I am logged in as "(.+)"$/ do |user|
visit "/login"
fill_in "login", :with => user
fill_in "password", :with => "123456"
click_button "Login"
end
When /^I am viewing "(.+)"$/ do |url|
visit(url)
end
Then /^I should see "(.+)"$/ do |text|
response_body.should =~ /#{text}/
end
On success a cookie is created
response.set_cookie(cookie_name, coockie_value)
and then verified in views when user tries to access admin pages via helper method:
def logged_in?
request.cookies[cookie_name] == cookie_value
end
And it looks like Webrat doesn't store cookies. Tests don't report any error, but "logged_in?" in views is always false, like the cookie was not saved.
Am I doing something wrong? If this is just how Webrat works, what is the best workaround?
The real problem is the way Sinatra is treating sessions in the test environment. Search the Google group for the discussion, but the real solution is to simply use:
use Rack::Session::Cookie
and not
enable :sessions
Using Selenium is nice but it's overkill as a solution for the OP's problem.
The workaround is use Webrat with Selenium back end. It runs all tests in a separate Firefox window, so cookies or javascript is not a problem. The downside is extra time and resources required to run Firefox and do all the real clicks, rendering etc.
You could have your "Given /^I am logged in" step hack logged_in?:
Given /^I am logged in as "(.+)"$/ do |user|
visit "/login"
fill_in "login", :with => user
fill_in "password", :with => "123456"
click_button "Login"
ApplicationController.class_eval <<-EOE
def current_user
#current_user ||= User.find_by_name(#{EOE})
end
end
EOE
end
There are two downsides:
It's really hackish to mix view-level and controller-level issues like this.
It'll be difficult to mock up "logout"

Resources