Faking instance variable in RSpec and Capybara feature spec - ruby-on-rails

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.

Related

'assigns' method not found in rspec capybara

I have the following code in my controller:
private
def remaining_words
#remaining_words = Vocab.all.where.not(id: session[:vocab_already_asked])
#questions_remaining = #remaining_words.length - 4
#quiz_words = #remaining_words.shuffle.take(4)
And here is my test:
feature 'Quiz functionality' do
scenario "gets 100% questions right in quiz" do
visit(root_path)
visit(start_quiz_path)
assigns(:questions_remaining).length.to_i.times do
orig_value = find('#orig', visible: false).value
choose(option: orig_value)
click_on('Submit')
expect(page).to have_content('You got it right!')
expect(page).not_to have_content('Sorry, wrong answer!')
end
expect(page).to have_content("Your score is 27/27")
save_and_open_page
end
end
I get the error message when I run the test:
NoMethodError: undefined method `assigns' for #<RSpec::ExampleGroups::QuizFunctionality:0x007f8f2de3f2b0>
# ./spec/features/quizzes_spec.rb:9:in `block (2 levels) in <top (required)>'
I've also tried using controller.instance_variable_get(:remaining_words) and get this error message
NameError:
undefined local variable or method `controller' for #<RSpec::ExampleGroups::QuizFunctionality:0x007fc4b99251a0>
Am I missing something in setting up the test? Should I be using describe instead of feature to enable the assign method?
assigns was solely available in controller tests - it was depreciated in Rails 5.
Testing what instance variables are set by your controller is a bad
idea. That's grossly overstepping the boundaries of what the test
should know about. You can test what cookies are set, what HTTP code
is returned, how the view looks, or what mutations happened to the DB,
but testing the innards of the controller is just not a good idea.
- David Heinemeier Hansson
In RSpec controller specs wrap the deprecated ActionController::TestCase.
A controller spec is identified by having the type: :controller metadata.
RSpec.describe ThingsController, type: :controller do
# ...
describe "GET #index" do
end
end
If you have set config.infer_spec_type_from_file_location! in config.infer_spec_type_from_file_location! RSpec will infer that any spec in spec/controllers has type: :controller.
You should avoid controller specs for new applications in favor of request and feature specs. One of the main problems with controller specs besides the violation of encapsulation is that the entire request phase is stubbed, the request does not actually go through rack or the routes which can mask routing errors and means that Rack middleware like Warden (used by Devise) or sessions must be stubbed.
If you have a legacy application you can reintroduce assigns with a gem. If you are just learning RSpec you should select more up to date tutorials.
Feature specs are high-level tests meant to exercise slices of
functionality through an application. They should drive the
application only via its external interface, usually web pages.
https://relishapp.com/rspec/rspec-rails/v/3-7/docs/feature-specs
Use feature specs for high level tests centered on the user story. Use RSpec.feature "New Cool Feature" to write a feature spec.
Request specs provide a thin wrapper around Rails' integration tests,
and aredesigned to drive behavior through the full stack, including
routing (provided by Rails) and without stubbing (that's up to you).
https://relishapp.com/rspec/rspec-rails/v/3-7/docs/request-specs/request-spec
Use RSpec.describe "Some resource", type: :request to write a feature spec.
Request specs are invaluable for testing API' or when you just need fast tests that ensure that the correct mutations happened to the DB or that the correct http responses are sent.
See:
https://blog.bigbinary.com/2016/04/19/changes-to-test-controllers-in-rails-5.html
https://github.com/rails/rails/issues/18950
You're writing feature specs/integration tests which don't have access to the controller/controller instance variables. They are meant to be more of a black box test executed from the users perspective. When setting up the data for the test you should know how many questions need to be asked and then either hardcode that in your test, or, better yet, detect based on the page contents whether there are more questions to answer (just like a user would have to).

Capybara webkit doesn't pass params from angular

I am trying to port a selenium test suite to capybara-webkit. The Rails app has an angular app embedded in the rails views and is not behaving as expected with webkit.
A test like this:
require 'spec_helper'
feature 'Editing company profiles' do
before do
#user = create(:employee)
#company = Company.find(#user.employer.id)
sign_in_as! #user
end
scenario 'successfully', js: true do
click_link 'Dashboard'
click_link #company.name
click_button 'Edit'
fill_in 'company_name', with: 'new name'
click_button 'Save'
expect(page).to have_content "Your company profile has been updated!"
end
end
Will pass without issue in selenium, but with webkit I get the error
Failure/Error: Unable to find matching line from backtrace
ActionController::ParameterMissing:
param is missing or the value is empty: company
# ./app/controllers/api/v1/companies_controller.rb:23:in `company_params'
# ./app/controllers/api/v1/companies_controller.rb:10:in `update'
The trace is missing, maybe because it's from angular land, but the error is reporting that no params are coming from the client. I've tried the capybara-angular gem, but it has not helped. I've also tried saving the page with capybara and nothing looks out of place there, are there any ways to access the PATCH request inside of webkit that's being generated in this test? I've also gotten similar errors with poltergeist.
Has anyone setup headless rspec testing with angular + rails? Any tips on how to debug why data isn't being sent over from the client?
Without seeing all of your code, this feels like it could be a problem associated with a known issue in the capybara-webkit gem is unable to pass entity bodies to the server.
I suspect that the update request is being sent as a PATCH request (which is appropriate), but the issue with the gem results in failure for your tests.
A workaround to your problem is to change the method of the request to PUT or POST, the issue linked above shows some options. You will be able to get your test to pass, but it's up to you to decide if changing the request type is worth getting your test to pass.
Note: In practice it may not matter if you don't actually use PATCH, as you could technically use (some of) the other http methods interchangeably -- but use caution as there are reasons to use a specific http method for a given situation. See this rubyonrails.org post from a few years ago for some details.

Error when using RSpec's `all` matcher with Capybara's `have_css` matcher

I'm just getting started with feature specs using RSpec (and Capybara). I'm testing my ActiveAdmin dashboard and I want to check that all panels have an orders table as shown in this snippet:
feature 'admin dashboard', type: :feature do
def panels
page.all('.column .panel')
end
describe 'all panels' do
it 'have an orders table' do
expect(panels).to all(have_css('table.orders tbody'))
end
end
end
I've used the all matcher a lot in my unit tests but it doesn't appear to work when wrapping Capybara's have_css matcher because I'm getting the following error:
Failure/Error: expect(panels).to all(have_css('table.orders tbody'))
TypeError:
no implicit conversion of Capybara::RackTest::CSSHandlers into String
Am I correct in my assumption that RSpec's built-in all matcher should work with other matchers as well?
Note: I'm using describe and it instead of feature and scenario in this instance because I'm testing output rather than user interaction scenarios (see my other question).
Unfortunately there is a conflict between RSpec's all and Capybara's all see Capybara Issue 1396. The all that you are calling is actually Capybara's all.
Solution 1 - Call BuiltIn::All Directly
The quickest solution would be to call RSpec's all method directly (or at least that code that it executes.
The expectation will work if you use RSpec::Matchers::BuiltIn::All.new instead of all:
expect(panels).to RSpec::Matchers::BuiltIn::All.new(have_css('table.orders tbody'))
Solution 2 - Redefine all
Calling the BuiltIn:All directly does not read nicely so might get annoying if used often. An alternative would be to re-define the all method to be RSpec's all method. To do this, add the module and configuration:
module FixAll
def all(expected)
RSpec::Matchers::BuiltIn::All.new(expected)
end
end
RSpec.configure do |c|
c.include FixAll
end
With the change, the all in the following line will behave like RSpec's all method.
expect(panels).to all(have_css('table.orders tbody'))
Note that if you want to use Capybara's all method, you would now always need to call it using the session (ie page):
# This will work because "page.all" is used
expect(page.all('table').length).to eq(2)
# This will throw an exception since "all" is used
expect(all('table').length).to eq(2)
I used a very similar approach to the accepted answer, but in a Cucumber environment I was getting errors about RSpec.configure not existing. Also, I wanted to call the matcher something besides all so that I could use them both without conflicts. This is what I ended up with
# features/support/rspec_each.rb
module RSpecEach
def each(expected)
RSpec::Matchers::BuiltIn::All.new(expected)
end
end
World(RSpecEach) # extends the Cucumber World environment
Now I can do things like:
expect(page.all('#employees_by_dept td.counts')).to each(have_text('1'))

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

Stubbing helpers rspec

I have a method in application_helper that is called admin_rights? to check if a user should be able to add content to the site. I haven't implemented a user system so it only returns true at the moment. But I am trying to test it, but I can't seem to find out how to stub it out so it returns false in the test. The spec checks for a link that should only be visible when admin_rights? returns true. When i test it manually by changing admin_rights? to false, it works as intended. So I am apparently not stubbing it out correctly.
The Spec is:
context "no admin rights" do
before do
page.stub(:admin_rights?).and_return(false)
visit fencers_path
end
it "should not have add fencer link" do
expect(page).not_to have_link('+ Fekter', href: new_fencer_path)
end
end
I'm looking for the correct way to stub it out or an alternative way to test it.
The test case you posted is an acceptance test. It boots up a server instance and goes through the full stack. You should really not rely on stubbing and mocking in these kind of tests. They should ensure that the application as a whole works and should treat your application as a black box. To replace tiny bits of code is a recipe for very brittle acceptance tests. Also if you run your tests with a driver that runs Javascript then there is no chance to get the stubbing to work because the server runs in a different process than your tests do.
You should implement the logic for admin_rights? and then tune your acceptance test-setup that the logic actually returns false. For example sign in with a normal user, which does not have admin rights. In the end you want your acceptance tests to match closely to the real world scenario.

Resources