Testing Rails helper with Rspec: undefined local variable or method `request' - ruby-on-rails

I have a helper method that uses 'request' to determine the URL. However, rspec can't seem to find request. I thought request was available to all front-facing tests?
How can I account for the request method in my spec?
Helper Spec
require 'spec_helper'
describe ApplicationHelper do
describe "full_title" do
it "should include the page title" do
expect(full_title("help")).to include('help')
end
end
end
Helper methods
def full_title(page_title)
if staging? # causing the issue
base_title = "Staging"
else
base_title = "Company Name"
end
if page_title.empty?
"#{base_title} | Tag line "
else
"#{base_title} | #{page_title} "
end
end
def staging? # the request here seems to be the problem
request.original_url.include? "staging"
end
Rspec error
Failure/Error: expect(full_title("help")).to include('help')
NameError:
undefined local variable or method `request' for #<RSpec::ExampleGroups::ApplicationHelper_2::FullTitle:0x00000106260078>
Thanks in advance.

First off: request is only available in the controller tests (and even then only in the request specs I think), helper tests are really basic and isolated. Which is good. Your helper code should be really minimal and normally only work on the input it receives.
However this is pretty easily solvable by using stubbing.
So write something like
#note, OP needed to replace 'helper' with 'self'for Rails 4.0.0 and Rspec 3.0
require 'rails_helper'
describe ApplicationHelper do
describe "full_title" do
context "in staging" do
it "should include the page title" do
helper.should_receive(:staging?).and_return(true)
expect(full_title("help")).to include('help')
end
end
context "not in staging" do
it "should include the page title" do
helper.should_receive(:staging?).and_return(false)
expect(full_title("help")).to include('help')
end
end
end
end
Which is imho a very clear, and then you write separate tests for your staging? method:
describe "staging?" do
context "when in staging" do
it "returns true" do
helper.stub(:request) { OpenStruct.new(original_url: 'staging') }
expect( helper.staging? ).to be true
end
end
context "when not in staging" do
it "returns false" do
helper.stub(:request) { OpenStruct.new(original_url: 'development') }
expect(helper.staging?).to be false
end
end
end
end
Some small remarks: ruby default indentation is 2 spaces.
Secondly, your function now literally says return true if true, ideally it should be written like
def staging?
request.original_url.include? "staging"
end

Related

Passing a clearer description to RSpec `its` method

I'm an RSpec newb, but am really loving how easy it is to write the tests and I'm continually refactoring them to be cleaner as I learn new features of RSpec. So, originally, I had the following:
describe Account do
context "when new" do
let(:account) { Account.new }
subject { account }
it "should have account attributes" do
subject.account_attributes.should_not be_nil
end
end
end
I then learned about the its method, so I tried to rewrite it as such:
describe Account do
context "when new" do
let(:account) { Account.new }
subject { account }
its(:account_attributes, "should not be nil") do
should_not be_nil
end
end
end
This fails due to its not accepting 2 arguments, but removing the message works just fine. The issue is that if the test fails, the message under the Failed examples section just says
rspec ./spec/models/account_spec.rb:23 # Account when new account_attributes
which isn't overly helpful.
So, is there a way to pass a message to its, or better yet, have it output a sane message automatically?
You could define an RSpec custom matcher:
RSpec::Matchers.define :have_account_attributes do
match do |actual|
actual.account_attributes.should_not be_nil
end
failure_message_for_should do
"expected account_attributes to be present, got nil"
end
end
describe Account do
it { should have_account_attributes }
end
You can also write: its(:account_attributes) { should_not be_nil }
See https://www.relishapp.com/rspec/rspec-core/v/2-14/docs/subject/attribute-of-subject
Take note that "its" will be extracted from rspec-core to a gem with the release of rspec 3, though.
Looks like a relatively simple monkey-patch will enable what you seek.
Look at the source of the rspec-core gem version you're using. I'm on 2.10.1. In the file lib/rspec/core/subject.rb I see the its method defined.
Here's my patched version - I changed the def line and the line after that.
Caution - this is very likely to be version specific! Copy the method from your version and modify it just like I did. Note that if the rspec-core developers do a major restructuring of the code, the patch may need to be very different.
module RSpec
module Core
module Subject
module ExampleGroupMethods
# accept an optional description to append
def its(attribute, desc=nil, &block)
describe(desc ? attribute.inspect + " #{desc}" : attribute) do
example do
self.class.class_eval do
define_method(:subject) do
if defined?(#_subject)
#_subject
else
#_subject = Array === attribute ? super()[*attribute] : _nested_attribute(super(), attribute)
end
end
end
instance_eval(&block)
end
end
end
end
end
end
end
That patch can probably be put in your spec_helper.rb.
Now the usage:
its("foo", "is not nil") do
should_not be_nil
end
Output on failure:
rspec ./attrib_example_spec.rb:10 # attr example "foo" is not nil
If you omit the second arg, the behavior will be just like the unpatched method.

How do I simulate a login with RSpec?

I have been playing with Rails for a couple of years now and have produced a couple of passable apps that are in production. I've always avoided doing any testing though and I have decided to rectify that. I'm trying to write some tests for an app that I wrote for work that is already up and running but undergoing constant revision. I'm concerned that any changes will break things so I want to get some tests up and running. I've read the RSpec book, watched a few screencasts but am struggling to get started (it strikes me as the sort of thing you only understand once you've actually done it).
I'm trying to write what should be a simple test of my ReportsController. The problem with my app is that pretty much the entire thing sits behind an authentication layer. Nothing works if you're not logged in so I have to simulate a login before I can even send forth a simple get request (although I guess I should write some tests to make sure that nothing works without a login - I'll get to that later).
I've set up a testing environment with RSpec, Capybara, FactoryGirl and Guard (wasn't sure which tools to use so used Railscasts' suggestions). The way I've gone about writing my test so far is to create a user in FactoryGirl like so;
FactoryGirl.define do
sequence(:email) {|n| "user#{n}#example.com"}
sequence(:login) {|n| "user#{n}"}
factory :user do
email {FactoryGirl.generate :email}
login {FactoryGirl.generate :login}
password "abc"
admin false
first_name "Bob"
last_name "Bobson"
end
end
and then write my test like so;
require 'spec_helper'
describe ReportsController do
describe "GET 'index'" do
it "should be successful" do
user = Factory(:user)
visit login_path
fill_in "login", :with => user.login
fill_in "password", :with => user.password
click_button "Log in"
get 'index'
response.should be_success
end
end
end
This fails like so;
1) ReportsController GET 'index' should be successful
Failure/Error: response.should be_success
expected success? to return true, got false
# ./spec/controllers/reports_controller_spec.rb:13:in `block (3 levels) in <top (required)>'
Interestingly if I change my test to response.should be_redirect, the test passes which suggests to me that everything is working up until that point but the login is not being recognised.
So my question is what do I have to do to make this login work. Do I need to create a user in the database that matches the FactoryGirl credentials? If so, what is the point of FactoryGirl here (and should I even be using it)? How do I go about creating this fake user in the testing environment? My authentication system is a very simple self-made one (based on Railscasts episode 250). This logging in behaviour will presumably have to replicated for almost all of my tests so how do I go about doing it once in my code and having it apply everywhere?
I realise this is a big question so I thank you for having a look.
The answer depends on your authentication implementation. Normally, when a user logs in, you'll set a session variable to remember that user, something like session[:user_id]. Your controllers will check for a login in a before_filter and redirect if no such session variable exists. I assume you're already doing something like this.
To get this working in your tests, you have to manually insert the user information into the session. Here's part of what we use at work:
# spec/support/spec_test_helper.rb
module SpecTestHelper
def login_admin
login(:admin)
end
def login(user)
user = User.where(:login => user.to_s).first if user.is_a?(Symbol)
request.session[:user] = user.id
end
def current_user
User.find(request.session[:user])
end
end
# spec/spec_helper.rb
RSpec.configure do |config|
config.include SpecTestHelper, :type => :controller
end
Now in any of our controller examples, we can call login(some_user) to simulate logging in as that user.
I should also mention that it looks like you're doing integration testing in this controller test. As a rule, your controller tests should only be simulating requests to individual controller actions, like:
it 'should be successful' do
get :index
response.should be_success
end
This specifically tests a single controller action, which is what you want in a set of controller tests. Then you can use Capybara/Cucumber for end-to-end integration testing of forms, views, and controllers.
Add helper file in spec/support/controller_helpers.rb and copy content below
module ControllerHelpers
def sign_in(user)
if user.nil?
allow(request.env['warden']).to receive(:authenticate!).and_throw(:warden, {:scope => :user})
allow(controller).to receive(:current_user).and_return(nil)
else
allow(request.env['warden']).to receive(:authenticate!).and_return(user)
allow(controller).to receive(:current_user).and_return(user)
end
end
end
Now add following lines in spec/rails_helper.rb or spec/spec_helper.rb
file
require 'support/controller_helpers'
RSpec.configure do |config|
config.include Devise::TestHelpers, :type => :controller
config.include ControllerHelpers, :type => :controller
end
Now in your controller spec file.
describe "GET #index" do
before :each do
#user=create(:user)
sign_in #user
end
...
end
Devise Official Link
The easiest way to login with a user on feature tests is to use the Warden's helper #login_as
login_as some_user
As I couldn't make #Brandan's answer work, but based on it and on this post, I've came to this solution:
# spec/support/rails_helper.rb
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } # Add this at top of file
...
include ControllerMacros # Add at bottom of file
And
# spec/support/controller_macros.rb
module ControllerMacros
def login_as_admin
admin = FactoryGirl.create(:user_admin)
login_as(admin)
end
def login_as(user)
request.session[:user_id] = user.id
end
end
Then on your tests you can use:
it "works" do
login_as(FactoryGirl.create(:user))
expect(request.session[:user_id]).not_to be_nil
end
For those who don't use Devise:
spec/rails_helper.rb:
require_relative "support/login_helpers"
RSpec.configure do |config|
config.include LoginHelpers
end
spec/support/login_helpers.rb:
module LoginHelpers
def login_as(user)
post "/session", params: { session: { email: user.email, password: "password" } }
end
end
and in the specs:
login_as(user)

Why i can not get current_user while writing test case with Rspec and Capybara

I have to write integration test case for my one feature listing page and that feature index method has code like below
def index
#food_categories = current_user.food_categories
end
Now when i try to write a test case for this it throws an error
'undefined method features for nil class' because it can not get the current user
Now what i have do is below
I have write the login process in the before each statement and then write the test case for the features listing page
Can you please let me know that how i can get the current_user ?
FYI, I have used devise gem and working on integration test case with Rspec
Here is my spec file
And here is my food_categories_spec.rb
Update: you confuse functional and integration tests. Integration test doesn't use get, because there's no controller action to test, instead you must use visit (some url). Then you have to examine content of a page, not response code (latter is for functional tests). It may look like:
visit '/food_categories'
page.should have_content 'Eggs'
page.should have_content 'Fats and oils'
In case you'll need functional test, here's an example:
# spec/controllers/your_controller_spec.rb
describe YourController do
before do
#user = FactoryGirl.create(:user)
sign_in #user
end
describe "GET index" do
before do
get :index
end
it "is successful" do
response.should be_success
end
it "assings user features" do
assigns(:features).should == #user.features
end
end
end
# spec/spec_helper.rb
RSpec.configure do |config|
#...
config.include Devise::TestHelpers, :type => :controller
end

How do you "nest" or "group" Test::Unit tests?

RSpec has:
describe "the user" do
before(:each) do
#user = Factory :user
end
it "should have access" do
#user.should ...
end
end
How would you group tests like that with Test::Unit? For example, in my controller test, I want to test the controller when a user is signed in and when nobody is signed in.
You can achieve something similar through classes. Probably someone will say this is horrible but it does allow you to separate tests within one file:
class MySuperTest < ActiveSupport::TestCase
test "something general" do
assert true
end
class MyMethodTests < ActiveSupport::TestCase
setup do
#variable = something
end
test "my method" do
assert object.my_method
end
end
end
Test::Unit, to my knowledge, does not support test contexts. However, the gem contest adds support for context blocks.
Shoulda https://github.com/thoughtbot/shoulda although it looks like they've now made the context-related code into a separate gem: https://github.com/thoughtbot/shoulda-context
Using shoulda-context:
In your Gemfile:
gem "shoulda-context"
And in your test files you can do things like (notice the should instead of test:
class UsersControllerTest < ActionDispatch::IntegrationTest
context 'Logged out user' do
should "get current user" do
get api_current_user_url
assert_response :success
assert_equal response.body, "{}"
end
end
end

Dynamically test each method in a controller

I have an Ruby on Rails 3 admin_controller with the default set of CRUD, index and so on methods. I'd like to test each of these for certain assertions with rspec.
Like response.should render_template("layouts/some_layout") or tests that it should require login.
Copy-pasting that test into the group of tests for each method is a lot of duplication. IMO it makes little sense to have an
it 'should require login' do
Duplicated several times troughout that test.
Is there a simple way to run a test on a list of methods? Say defined_methods.each do |method| it 'should' .... of some sort?
Is this a good way in the first place? Or am I taking a wrong route in the first place?
Given that you really want all those assertions, have you considered shared example groups?
shared_examples_for "an action that requires authentication" do
it "should render successfuly" do
sign_in(user)
response.should be_success # or whatever
end
it "should deny access" do
# don't sign_in the user
# assert access was denied
end
end
shared_examples_for "another behaviour" do
# ...
end
let(:user) { create_user }
describe "#index" do
before(:each) { get :index }
it_behaves_like "an action that requires authentication"
it_behaves_like "another behaviour"
end
describe "#show" do
before(:each) { get :show }
it_behaves_like "an action that requires authentication"
end
# ...
Of course before writing large number of specs for a basic functionality you should always check if it isn't already tested by the library that is providing the functionality (e.g. checking for the rendered template, if it is handled by rails's implicit rendering, might be a bit overkill).
If you wanted to go down the route of iteratively testing each public method in the controller, you could do something like:
SomeController.public_instance_methods(false).each do |method|
it "should do something"
end
However, I think a shared example group (see about half way down this page: http://rspec.info/documentation/) would be prettier. If it were extracted so it could be used across all your controller specs, it'll be even nicer..
shared_examples_for "admin actions" do
it "should require login"
end
Then in each controller spec:
describe SomeController do
it_should_behave_like "admin actions"
end
Just add it to your test_helper.rb, something like:
def requires_login
...
end

Resources