Devise login acceptance test with capybara - ruby-on-rails

There are already a few questions on stackoverflow with possible solutions, but none of them seemed to solve my problem, thus I'm asking another one, in hope it's specific enough to not be closed as duplicate.
I am trying to test a customized login form that uses devise. The environment consists of
rails 6.0
rspec-rails 3.9
capybara 3.31
selenium-webdriver 3.142.7
database_cleaner 1.8
The following spec is failing due to a 401 Unauthorized response, rendering the login form again with an error message that the credentials were wrong.
require 'rails_helper'
feature 'Logging in' do
background do
FactoryBot.create :user, email: 'john.doe#example.com',
first_name: 'John',
password: 'password',
password_confirmation: 'password'
end
scenario 'with correct credentials' do
visit new_user_session_path
within('#user-sessions--new') do
find('#user_email').fill_in with: 'john.doe#example.com'
find('#user_password').fill_in with: 'password'
end
find('input[name="commit"]').click
expect(page).to have_content 'John'
end
end
Checking the test.log after temporarily disabling the parameter filters for logging, I can see that the password has been submitted properly, and the user is being created before the login attempt happens, yet the authentication fails. Entering an interactive debugging session and trying to log in manually inside the capybara's spun up browser, the login also fails. The email/password login works in development mode when creating the same user through the console, though. Right now, no other devise features that could impact the login behavior (like confirmable) are used.
Most of the questions I found so far advise the following points:
Disable transactional fixtures: My rails_helper.rb already contains this;
RSpec.configure do |config|
config.use_transactional_fixtures = false
end
Configure Database Cleaner: This was suggested in Failing to test Devise with Capybara and was also already the case before I started to implement capybara specs;
RSpec.configure do |config|
config.before(:suite) do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(
:truncation,
except: %w[ar_internal_metadata schema_migrations]
)
end
config.before :each do
DatabaseCleaner.start
end
config.after :each do
DatabaseCleaner.clean
end
end
Right now I have no other Idea why this scenario could be failing except for maybe the environment in which the tests are run hot having a proper database connection. If someone has an idea that helps, I'd be happy to hear it.

When using Rails > 5.0 it safely shares the database connection between the tests and the app under test Capybara starts. Because of that you don't need database cleaner, and you should be using transactional tests for the speed and isolation benefits. Start by removing all references to database cleaner from your project and re-enabling transactional fixtures (the default). If that doesn't solve your issue then there's a couple of other potential issues.
Your factory isn't creating a valid user - Use the FactoryBot.lint functionality to verify all your factories produce valid objects before your tests - https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#linting-factories
You have a JS error on your page that is causing issues. In development mode each JS asset is served separately which means an error in one doesn't affect the others. In test mode, however, the assets are all concatenated which means an error in one can cause JS in other assets not to be run. Check your browser console for any JS errors and fix them.
If none of that helps please add relevant portions of your test.log to your question
Note: You're writing your tests more verbosely than they need to be. If locating fields to fill_in by id you can just pass the id to fill_in and not need to use separate find calls for every element, you should also use the more semantic click_button when the element you're attempting to click qualifies as a button
within('#user-sessions--new') do
fill_in 'user_email', with: 'john.doe#example.com'
fill_in 'user_password', with: 'password'
end
click_button('commit')
expect(page).to have_content 'John'

Related

Rails 5 - Unable to Cleanse Test DB

I've recently started testing my app using RSpec. Being the testing noob that i am, i did not install the full suite of tools that people normally use (namely FactoryGirl, also now known as FactoryBot. And DatabaseCleaner).
Anyway here's what happened. After getting my feet wet with RSpec, i started using FactoryBot so that my tests looks less convoluted. Note that :user is a Devise object.
# spec/models/first_test_spec.rb
FactoryBot.create(:user)
#Other Code
So i'm doing stuff with my first_test_spec.rb and i run it and everything is working perfectly fine. I then create my 2nd spec file
# spec/models/second_test_spec.rb
FactoryBot.create(:user)
#Other Code
Now here comes the problem. My tests are failing because FactoryBot.create(:user) is invalid, due to Devise requiring unique emails. Clearly this indicates that the data from my first_test_spec is persisting hence the error.
So i attempt to install DatabaseCleaner and hopefully clear my Test DB after each run. My rails_helper looks like this:
require 'spec_helper'
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
abort("The Rails environment is running in production mode!") if Rails.env.production?
require 'rspec/rails'
require 'support/factory_bot'
# Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
ActiveRecord::Migration.maintain_test_schema!
RSpec.configure do |config|
config.fixture_path = "#{::Rails.root}/spec/fixtures"
config.use_transactional_fixtures = false
config.before(:suite) do
DatabaseCleaner.clean_with(:truncation)
end
config.before(:each) do
DatabaseCleaner.strategy = :transaction
end
config.before(:each, js: true) do
DatabaseCleaner.strategy = :truncation
end
config.before(:each) do
DatabaseCleaner.start
end
config.after(:each) do
DatabaseCleaner.clean
end
config.infer_spec_type_from_file_location!
config.filter_rails_from_backtrace!
end
I think i've got everything set up correctly, so i'm uncertain if the errors are still occurring due to my DatabaseCleaner being set up wrongly.
Anyway, running rspec still throws the same error, where second_test_spec cannot create the user object because the email already exists (a validation that comes with Devise itself).
I then proceed to run the following:
rake db:test:prepare
rspec spec/models/second_test_spec.rb
And it still throws the same error. So right now, i have no idea if my database is being cleanse after running rspec. But i'm quite certain that i have been unable to purge my test database.
So i guess i really have 2 questions/problems:
1) How do you purge your test database? (Googling reveals that rake db:test:prepare is the way)
2) Is my DatabaseCleaner setup correctly? If so, shouldn't it be purging the database?
UPDATE:
As suggested to me in the comments, using sequence for creating unique fields with FactoryBot is recommended. Obviously that made the problem go away because there would no longer be validation errors from Devise.
I then went on to test a couple of things.
rails c test
I ran this to check my test database and it was indeed empty. This indicates that DatabaseCleaner is working perfectly fine. What i fail to understand then, is why the need to sequence my email creation in FactoryBot? Or i suppose, i fail to understand how does RSpec "compile & run".
puts #user.email
So i wanted to print out the emails to look at the sequencing to see if i'm able to decipher the problem. Here's what happens:
running rspec spec/models/first_test_spec.rb
Tutor email yields the number 2.
running rspec spec/models/second_test_spec.rb
Tutor email yields the number 3.
running rspec
Tutor email yields the numbers 2 & 5.
So i'm not sure if there are "problems" with my test suite. Clearly my original problem has been fixed, and this is a separate topic altogether. But i figured if anyone would like to explain this mystery to anyone else who chances upon this thread may wish to do so.
Seeing your full spec files would help. If you are running the specs as you've written - creating a user outside of an example group or a before block - that will cause records to be written outside of the cleaning strategy scope and can cause data to remain across examples. Your DBcleaner config looks to be set up fine otherwise.
rake db:test:prepare is the correct way to clean out your test db but shouldn't need to be ran often unless you have migration changes. You can jump into a rails console within the test environment rails c test and look around to see if there are any records left behind.
As a side note you can flip config.use_transactional_fixtures from false to true and remove DBcleaner altogether from your app. Just make sure there is no residual data in your database before going this route.

Faking instance variable in RSpec and Capybara feature spec

I'm trying to set up some feature specs before I get into refactoring some of my company's old code. It's kind of an unconventional setup, but I was able to figure out enough about test doubles to bypass the authentication enough to get started. One problem I'm still having is that some of the instance variables set in these methods I'm bypassing are expected by the view, so I get undefined method for nil:NilClass errors. I would like to get the specs running before I make any changes to the program code. In this case, I could easily just move the particular instance variable to another method. But I'm sure more situations like this will come up. Here's the example I'm currently working on:
def security_level
#right_now = Time.now
#
# other code that wont work without
# connecting to a remote authentication
# server
#
end
Then in my spec:
feature 'Navigation' do
before(:each) do
allow_any_instance_of(ApplicationController).to receive(:security_level).and_return(nil)
end
scenario 'is possible' do
visit root_path
expect(page.has_content?('Quick Stats'))
end
end
Here's the error, coming from #right_now.year in the view
Failure/Error: visit root_path
NoMethodError:
undefined method `year' for nil:NilClass
# ./common/views/layouts/bootstrap/layout.haml:63
EDIT: Is there a way to set instance variables on the controller from within a feature spec?
There's no easy way to accomplish what you want.
The feature spec is handled mostly by Capybara, not RSpec. Capybara runs the majority of the browser / rails server behavior in an external process. This make it inaccessible from RSpec's point-of-view. Thus you cannot use stubs / doubles in this manner.
Feature specs are largely meant to be end-to-end acceptance tests. The idea is to exercise your system as those who would use your system do. Generally, in these types of specs you perform various "workflows". This means, having the spec, log a user in, navigate to particular pages, filling forms, clicking buttons and links. You then generally make your expectations on what you see in the view.
This means your spec would look more like:
feature 'Navigation' do
let(:regular_user) { User.create!(name: 'A Regular User') }
def sign_in(a_user)
visit sign_in_url
# fill out form
click_button 'Sign In'
end
before(:each) do
sign_in(regular_user)
end
scenario 'is possible' do
visit root_path
expect(page.has_content?('Quick Stats'))
end
end
https://github.com/per-garden/fakeldap may provide enough ldap functionality for your feature tests.

Rspec is not creating database records accessible from poltergeist

I'm having terrible trouble getting Poltergeist and RSpec to play together nicely.
I've written the following test:
it "allows the trainer to view a runner" do
visit '/'
all(:xpath,'//a[#id="get-started"]').first.click
fill_in :name, with: "New Admin"
fill_in :email, with: "admin#test.org"
fill_in :password, with: "letmein"
fill_in :password_confirmation, with: "letmein"
all(:xpath,'//input[#id="get-started-submit"]').first.click
#runner_1 = FactoryGirl.create(:runner, name: "Axel", email: "axel#test.org")
visit '/runners/axel'
debugger
Effectively, what the above is doing is registering 'New Admin' with the password, 'letmein', then trying to view the runner profile page for 'Axel'.
Where the debugger interrupts, I can see that #runner_1 (Axel) has been created:
Runner.friendly.find('axel')
>> #<Runner id: 2, email: "axel.manzano#hotmail.fr",........>
However, when trying to visit '/runners/axel', Poltergeist reports:
ActiveRecord::RecordNotFound
It's not an issue with routes, or anything like that.
Having explored this bug a little further, in fact, it seems anything created in the test file doesn't actually get set up in the environment that Poltergeist is accessing.
I can't seem to understand why. Any help greatly appreciated.
Chances are, you are using "transactional fixtures" in rspec. This means that each test run in a database transaction, which is rolled back at the end of test, so that each test has a clean database.
Other threads/programs can not see what is going on in the transaction. Poltergeist runs the server in the separate thread, which means that it can not see anything that is written to the database in rspec (although it can be accessed directly from the rspec code).
There is a description of this phenomenon on the capybara homepage. The solution is to disable the transactional feature in rspec-rails and use something like DatabaseCleaner to reset the database after a test.
This will work, but unfortunately truncating or deleting the database contents is somewhat slower than they would with the transactional approach - this is why the tranasactions are the default in the first place.

Handle authentication with Capybara / Minitest for integration testing

I'm stuck trying to create integration tests using Capybara and MiniTest::Spec. I'm not using any 3rd party plugin for authentication. I'm using basic Authentication using has_secure_password built into rails 4.1
I have a helper that is looking for current_user which is created after authentication (pretty standard).
I've tried authenticating with Capybara then testing with visit:
test.rb
require 'test_helper'
describe "Admin area integration" do
setup do
def current_user
create(:admin_user, password: "test", password_confirmation: "test")
end
end
teardown do
current_user.destroy!
end
# results in error below
it "visits admin area path" do
visit admin_area_path
page.text.must_include('Dashboard')
end
# test passes
it "test user login" do
visit "/login"
within("#login_form") do
fill_in('email', with: current_user.email)
fill_in('password', with: "test")
end
click_button('login')
has_content?('Welcome')
end
end
Error
undefined method `email' for nil:NilClass app/helpers/application_helper.rb
Is there a way to pass the current_user object using capybara visit or am I missing something simple so the helper will not throw an error?
You are not supposed to modify internals of your Rails app, when doing integration tests. These tests should simulate the real world behaviour - a user visiting your site with a browser. So there is no way to pass the current_user object to capybara, like there is no way to modify the user session for your user from outside the app.
The straightforward way would be extracting the login steps(filling out the form) into separate function within some other test file( we usually have them all in test/support/** and just require all supporting functions in spec_helper). Then you repeat the login steps before any other test, which requires the user to be logged in.
However once we have tested the login, we can rely on it and the repetitive task of login the user each time can become quite annoying. It wouldn't be Ruby otherwise, when there wasn't a way to patch your app behaviour, while in test mode.
You can try using some mocking/stubbing lib and just stub the current_user method on any instance of the class which is holding it. Mocha example:
require 'mocha'
ApplicationController.any_instance.stubs(:current_user).returns(User.new {...})
The other option would be to modify the rack session directly. I expect your are storing the user_id in the session, and your current_user method just loads the user with that id.
So you can just require the rack_session_accessgem within your testsuite and set the user_id of your test user.
Remember also to disable transactional fixtures at least for the integration tests and use database_cleaner instead. Otherwise capybara will not be able to see any of your test data created, because it will be in an uncommitted transaction which is only accessible for the initiating thread.
See Configuring database_cleaner with Rails, RSpec, Capybara, and Selenium

Unable to locate element with cucumber step_definition, but able to locate it with capybara stand-alone

I am testing a website that has both a vertical login form on the right, and a horizontal form on the bottom. Both have identically named "email" and "password" fields. So I am employing capybara's within to scope for the fields I'm interested in. The interesting bits of the web form look like this:
I have two separate projects, in which I am experimenting with sauce labs for automation. The first project is the capybara-only example, modified to test the page shown above. The second project is a cucumber implementation of the exact same tests. These are very simple, one-time hard-coded tests, just to get a proof of concept of the two techniques.
Here is the interesting bit of the capybara-only example:
within(:css, ".right-container.login-form") do
fill_in 'email', :with => "greg.gauthier+#{generate_email_suffix}#website.com"
fill_in 'password', :with => 'l33tP#$$w0rd'
click_button 'Submit'
end
Here is the interesting bit of the cucumber step_definition:
When(/^The user enters his information$/) do
within(:css, ".right-container.login-form") do #the page has duplicate forms
fill_in 'email', :with => "greg.gauthier+#{generate_email_suffix}#website.com"
fill_in 'password', :with => 'l33tP#$$w0rd'
click_button 'Submit'
end
end
When I run the capybara-only version, everything works great. The form gets filled in, and the email confirmation gets sent.
However, when I run the cucumber version, I get this error:
Unable to find css ".right-container.login-form" (Capybara::ElementNotFound)
How can this be? It's the exact same page, the exact same capybara method (within, using the :css selector), and the exact same test code. What am I not getting (aside from the fact that I'm probably cuking it wrong)?
Oh, here's what the require list looks like in the sauce_helper:
Capybara-only version:
require "sauce"
require "sauce/capybara"
require 'capybara/rails'
require 'capybara/rspec'
Cucumber version:
require "sauce"
require "sauce/capybara"
require "sauce/cucumber"
Do I maybe need to include the extra capybara gems in the cucumber version?
Ok, I'm embarrassed to admit this, but the reason for this question was ignorance of webdriver behavior, at least as it works with Capybara/Cucumber.
The test in question is the second scenario in a set of scenarios. Apparently, selenium resets the browser to blank, between each scenario. So, test one works perfectly, but test two, three, and so forth fail on ElementNotFound because, of course... the page in question was not even loaded.
I added a Before hook to a hooks.rb file in my support path, and now it's working.
Apologies for cluttering the question stream...
Try adding require 'capybara' prior to require 'sauce' in your "Cumcumber Version"
From sauce GitHub
## In your test or spec helper
require "capybara"
require "sauce/capybara"
Reasoning within calls find. Sauce checks to see if a find method is defined an if so then it creates it's own alias called base_find if not it uses it's own find method which is not the same as capybara's. I think they may have something to do with your issue since it is using sauce's find method.
Not positive this is the answer but you can always give it a shot.

Resources