I'm building a Rails application and formulating tests using RSpec.
I wrote tests for a method I'm creating called current_link_to. This method is supposed to check whether the current page corresponds to the path I pass it and add the current class to the generated link in case it does.
Here is the spec:
require "spec_helper"
describe ApplicationHelper do
describe "#current_link_to" do
let(:name) { "Products" }
let(:path) { products_path }
let(:rendered) { current_link_to(name, path) }
context "when the given path is the current path" do
before { visit(path) }
it "should return a link with the current class" do
# Uses the gem "rspec-html-matchers" (https://github.com/kucaahbe/rspec-html-matchers)
expect(rendered).to have_tag("a", with: { href: path, class: "current" }) do
with_text(name)
end
end
end
context "when the given path is not the current path" do
before { visit(about_path) }
it "should return a link without the current class" do
expect(rendered).to have_tag("a", with: { href: path }, without: { class: "current" } ) do
with_text(name)
end
end
end
end
end
I then tried implementing my method following the spec:
module ApplicationHelper
def current_link_to(name, path, options={})
options.merge!({ class: "#{options[:class]} current".strip }) if current_page?(path)
link_to(name, path, options)
end
end
However, the tests fail with the following error:
Failure/Error: let(:rendered) { current_link_to(name, path) }
RuntimeError: You cannot use helpers that need to determine the current page unless your view context provides a Request object in a #request method
Since I don't really need the current_page? helper method to perform checks on the request, I decided that it would make sense to stub it.
I tried the following methods, but none of them worked:
helper.double(:current_page? => true)
Seems to stub the helper.current_page? method, but it's not the same method that's being called by my function.
allow(ActionView::Helpers::UrlHelper).to receive(:current_page?).and_return(true)
The stub seems not to be effective at all
While writing this question I stumbled onto the solution. I managed to stub the current_page? method using this in a before block:
allow(self).to receive(:current_page?).and_return(true)
It worked, however this solution raised more questions than it really answered. I am now baffled over how this works, as it seems weird that self in a before block would respond to current_page? and that said method would in fact be exactly the same one my helper is calling.
Even after reading documentation and trying to figure out how this works by littering my code with puts calls, the following doubts still haunt me:
Why are helper methods available directly in the specs, when the RSpec docs mention that they should instead be available as methods on the helper object available in all helper specs?
How does stubbing the current_page? method on self in a RSpec before block somehow reflect onto the actual method that gets called by my helper? Does self in my helper for some reason reference the same self you can find in the before block? Is RSpec or Rails including and mixing stuff under the covers?
If the same self encompasses my spec and my helpers, what exactly does self refer to in this case and why is it the same everywhere?
It would be great if someone could help me figure this out because this is blowing my mind up, and I'm scared of using code that I don't really understand.
With respect, you're testing a little too much functionality here. The trick is to test only the bits you need to test.
In this instance, you only need to test that the current class is added when it needs to be, and isn't when it doesn't need to be.
This code should do the trick for you:
require 'rails_helper'
# Specs in this file have access to a helper object that includes
# the ApplicationHelper.
RSpec.describe ApplicationHelper, type: :helper do
describe 'current_link_to' do
let(:subject) { helper.current_link_to('some_name', 'some_path', options = {}) }
context 'where the path is current' do
before do
allow(helper).to receive(:current_page?).and_return true
end
it 'should include the current class' do
expect(subject).to match /current/
end
end
context 'where the path is not current' do
before do
allow(helper).to receive(:current_page?).and_return false
end
it 'should not include the current class' do
expect(subject).to_not match /current/
end
end
end
end
I've been a little glib and only tested for the presence of 'current' in the returned string. You could test for something like 'class="current"' if you want to be more precise.
The other key is the comment at the top of the page, which Rails inserts into blank helper specs for you:
# Specs in this file have access to a helper object that includes
# the ApplicationHelper.
That means that you can use 'helper' where in your comment above you were using 'self', which makes things a little clearer (imho)
Hope it helps!
Related
In my Rails application I have a User model:
class User
def self.foo
User.all.each{ |user| user.bar }
end
def bar
end
end
In my spec file I want to check that foo calls bar for every user, so far that's what I have:
describe '::foo' do
let!(:users) { Fabricate.times(5, :user) }
it 'calls bar for every user' do
users.each do |user|
expect(user).to receive(:bar)
end
User.foo
end
end
Although the method gets called (I debugged it, so I'm sure of that) the spec is red.
Also I tried to write this code to understand where the problem was:
let!(:user) { Fabricate(:user) }
it 'fails' do
expect(user).to receive(:bar)
User.first.bar
end
it 'pass' do
expect(user).to receive(:bar)
user.bar
end
It seems that if I reference my instance directly it works, if I obtain it from the DB the expectation doesn't work.
I use mongoid, not sure if this is relevant.
I believe it cannot be done due to how RSpec works: When you set an expectation, RSpec essentially 'wraps' the object so that it can keep track of the messages it receives.
But when the implementation code fetches records from the database, they are not wrapped, so RSpec isn't able to record their messages.
RSpec does have a method allow_any_instance_of which can help in some cases, but its use is discouraged, and don't think it would be suitable here.
In this situation, I would suggest stubbing User.all to return some doubles (two should be sufficient). You can then verify that bar is called on each one.
I have a helper page_title_default in ApplicationHelper:
def page_title_default(options = {})
t '.title', options
end
Now I want to test it like this:
describe '#page_title' do
subject { page_title }
it { ... }
end
end
This results in the following error:
Cannot use t(".title") shortcut because path is not available
According to this post it should be possible to stub the #virtual_path variable like this:
helper.instance_variable_set(:#virtual_path, "admin.path.form")
But this doesn't seem to help: While I am able to stub it and then to call something like helper.t '.something' directly in the test, it doesn't work for the translation helper which is used in the page_title_default method (which still has #virtual_path set to nil). So it seems it's not the same instance of translation helper. But how can I find the page_title_default method one's?
How about something like:
RSpec.describe PageHelper, :type => :helper do
describe "#page_title_default" do
before do
allow(helper).to receive(:t).with(".title", {}) { "Hello!" }
end
subject { helper.page_title_default }
it { is_expected.to eq "Hello!" }
end
end
We're stubbing the "translated" string returned here to decouple the spec of helper from "real" translations, which may appear to be fragile for the test of PageHelper itself - the tests would fail every time you change the translations of ".title".
On the other hand - if you change the key used, eg. from ".title" to ".default_title" it should fail, because it is change of behaviour.
I think the proper text displayed should be tested on different level of test (integration tests, to be specific). Please, check the following answer.
Hope that helps!
I have a bunch of codes with repeating structures in a feature test in Rails. I would like to dry up my spec by reusing the structure. Any suggestions?
An example is:
feature "Search page"
subject { page }
it "should display results"
# do something
within "#A" do
should have_content("James")
end
within "#B" do
should have_content("October 2014")
end
# do something else
# code with similar structure
within "#A" do
should have_content("Amy")
end
within "#B" do
should have_content("May 2011")
end
end
At first, I tried to define a custom matcher in RSpec, but when I add within block, it did not seem to work. My guess is within is specific to Capybara, and cannot be used in custom matcher in RSpec.
Why not factor the common code into helper methods in a module. Then you can include that module in your spec_helper.rb file
I usually put common code like user_login in such a module in a file in the spec/support folder
spec_helper.rb
#Load all files in spec/support
Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
RSpec.configure do |config|
#some config
config.include LoginHelper
#more config
end
spec/support/login_helper.rb
module LoginHelper
def do_login(username, password)
visit root_path
within("#LoginForm") do
fill_in('username', :with => username)
fill_in('password', :with => password)
click_button('submit')
end
end
end
I don't think you're using within as a matcher, since a matcher would be used after a should, should_not, etc. You can load custom, non-matcher methods into your specs by writing a module and including it in your spec_helper.rb config block, e.g.:
spec/support/my_macros.rb
module MyMacros
def within(tag, &block)
# your code here
end
end
spec/spec_helper.rb
require 'spec/support/my_macros'
...
RSpec.configure do |config|
config.include MyMacros
...
end
I'm using Capybara + Cucumber for end-to-end testing. In the end, I think I've pretty much done what both #hraynaud and #eirikir suggest (directionally speaking) - although the details are different since I'm in the Cucumber context. So, consider this not a whole different idea - but maybe a slightly more complete description and discussion. Also, note that my examples focus on testing results - not navigation and form filling. Since it looked like you were in a testing mindset (given your use of should have_content), I thought this might be of interest.
In general, my approach is:
Wrap Capybara tests in validation helper methods within a module. The motivation for wrapping is (a) to save me from having to remember Capybara syntax and (b) to avoid having to type all those repetitive test statements. Also, it ends up making my tests cleaner and more readable (at least for me).
Create a generic validate method that receives (i) a validation helper method name (as a symbol) and (ii) an array of items each of which is to be passed to the validation helper. The validate method simply iterates over the array of items and calls the validation helper method (using the send method), passing each item along with each call.
Attach the helpers and generic validate method to World (read more about World here) so that they are available throughout my Cucumber tests.
Enjoy testing happiness!
Steps 1-3 happen in a file called form_validation_helpers.rb.
features/support/form_validation_helpers.rb
module FormValidationHelpers
...more methods before
# ============================================================================
# Tests that an element is on the page
# ============================================================================
def is_present(element)
expect(find(element)).to be_truthy
end
# ============================================================================
# Tests for the number of times an element appears on a page
# ============================================================================
def number_of(options={})
page.should have_css(options[:element], count: options[:count])
end
# ============================================================================
# Checks that a page has content
# ============================================================================
def page_has_content(content)
page.should have_content(content)
end
...more methods after
# ============================================================================
# The generic validation method
# ============================================================================
def validate(validation, *items)
items.each do |item|
send(validation, item)
end
end
end
World(FormValidationHelpers)
Step 4 (from above) happens in my step files.
features/step_definitions/sample_steps.rb
Then(/^she sees the organization events content$/) do
validate :number_of,
{element: 'ul#organization-tabs li.active', is: 1}
validate :is_present,
"ul#organization-tabs li#events-tab.active"
validate :page_has_content,
"A Sample Organization that Does Something Good",
"We do all sorts of amazing things that you're sure to love."
end
As you can see from the validate :page_has_content example, I can run the test multiple times by adding the appropriate arguments onto the validate call (since the validate method receives everything after the first argument into an array).
I like having very specific selectors in my tests - so I can be sure I'm testing the right element. But, when I start changing my view files, I start breaking my tests (bad) and I have to go back and fix all the selectors in my tests - wherever they may be. So, I made a bunch of selector helpers and attached them to World the same as above.
features/support/form_selectors_helpers.rb
module FormSelectorsHelper
...more _selector methods before
def event_summary_selector
return 'input#event_summary[type="text"]'
end
...more _selector methods after
end
World(FormSelectorsHelper)
So now, I have only one place where I need to keep my selectors up to date and accurate. Usage is as follows (note that I can pass whatever the validation helper method needs - strings, methods, hashes, arrays, etc.)...
features/step_definitions/more_sample_steps.rb
Then(/^she sees new event form$/) do
validate :is_present,
event_summary_selector,
start_date_input_selector,
start_time_input_selector,
end_time_input_selector
validate :is_absent,
end_date_input_selector
validate :is_unchecked,
all_day_event_checkbox_selector,
use_recur_rule_checkbox_selector
validate :is_disabled,
submit_button_selector
validate :has_text,
{ element: modal_title_bar_selector, text: "Okay, let's create a new event!" }
end
Turning back to your question, I imagine you could end up with something like:
feature "Search page"
subject { page }
it "should display results"
# do something
validate :has_content_within,
[a_selector, "James"],
[b_selector, "October 2014"]
# do something else
validate :has_content_within,
[a_selector, "Amy"],
[b_selector, "May 2011"]
end
Capybara Test Helpers provides a nice way to encapsulate test code when using Capybara + RSpec.
RSpec.feature "Search page", test_helpers: [:search] do
before do
visit search_path
end
it "should display results"
search.filter_by(name: 'James')
search.should.have_result(name: 'James', date: 'October 2014')
search.filter_by(name: 'Amy')
search.should.have_result(name: 'Amy', date: 'May 2011')
end
end
You can then implement your own actions and assertions as needed:
class SearchTestHelper < Capybara::TestHelper
aliases(
name_container: '#A',
date_container: '#B',
)
def filter_by(attrs)
attrs.each { |key, name| ... }
click_link('Search')
end
def have_result(name:, date:)
have(:name_container, text: name)
within(:date_container) { have_content(date) } # equivalent
end
end
You can read the guide here.
I am in the process of freeing myself from FactoryGirl (at least in the lib folder). So, I start writing strange stuff like "mock" and "stub". Can somebody help a novice out?
I have this module
module LogWorker
extend self
def check_todo_on_log(log, done)
if done == "1"
log.todo.completed = true
log.todo.save!
elsif done.nil?
log.todo.completed = false
log.todo.save!
end
end
end
log and todo are rails models with a todo :has_many logs association. But that should really not matter when working with stubs and mocks, right?
I have tried many things, but when I pass the mock to the method nothing happens,
describe LogWorker do
it 'should check_todo_on_log'do
todo = mock("todo")
log = mock("log")
log.stub!(:todo).and_return(todo)
todo.stub!(:completed).and_return(false)
LogWorker.check_todo_on_log(log,1)
log.todo.completed.should eq true
end
end
Failures:
1) LogWorker should check_todo_on_log
Failure/Error: log.todo.completed.should eq true
expected: true
got: false
(compared using ==
I would really like to see some spec that would test the LogWorker.check_todo_on_log method with stubs and/or mocks.
Firstly, your check_todo_on_log method is pretty bad. Never, ever use strings as options, especially when the string is "1". Also, if you pass "2", nothing happens. I'll assume though it is just a partial method, and your code isn't really like that :P
Looking at your code, you have three main problems. Firstly, you call LogWorker.check_todo_on_log(log,1). This won't do anything, as your method only does stuff when the second param is the string "1" or nil. Secondly, you stub todo.completed so it always returns false: todo.stub!(:completed).and_return(false). You then test if it is true. Obviously this is going to fail. Finally, you don't mock the save! method. I don't know how the code is actually running for you (it doesn't work for me).
Below is how I would write your specs (note that they are testing weird behaviour as the check_todo_on_log method is also strange).
Firstly, there is an easier way to add mock methods to a mock object. You can pass keys and values to the mock methods, and they will automatically be created.
Next, I put the mocks into let blocks. This allows them to be recreated easily for each test. Finally, I add a test for each possible behaviour of the function.
# you won't need these two lines, they just let the code be run by itself
# without a rails app behind it. This is one of the powers of mocks,
# the Todo and Log classes aren't even defined anywhere, yet I can
# still test the `LogWorker` class!
require 'rspec'
require 'rspec/mocks/standalone'
module LogWorker
extend self
def check_todo_on_log(log, done)
if done == "1"
log.todo.completed = true
log.todo.save!
elsif done.nil?
log.todo.completed = false
log.todo.save!
end
end
end
describe LogWorker do
let(:todo) { mock("Todo", save!: true) }
let(:log) { mock("Log", todo: todo) }
describe :check_todo_on_log do
it 'checks todo when done is "1"'do
todo.should_receive(:completed=).with(true)
LogWorker.check_todo_on_log(log,"1")
end
it 'unchecks todo when done is nil'do
todo.should_receive(:completed=).with(false)
LogWorker.check_todo_on_log(log,nil)
end
it "doesn't do anything when done is not '1' or nil" do
todo.should_not_receive(:completed=)
LogWorker.check_todo_on_log(log,3)
end
end
end
Notice how I am using behaviour based testing? I'm not testing that an attribute on the mock has a value, I am checking that an appropriate methods are called on it. This is the key to correctly using mocks.
I use a method named "generate_coordinate" (located in the app/helpers/planets_helper.rb) in my controller PlanetsController.
When running tests, it seems that rspec isn't able to access it, and so cause my test suite to fail because the planet doesn't have any coordinates.
I tried to include my helper at the beginning of the utilities.rb file, but it didn't work
include ApplicationHelper
include PlanetsHelper
I also tried to write my method inside the utilities.rb file, without more success.
I read this post "Where/how to include helper methods for capybara integration tests", but it didn't help me.
I also read about "stub" functions, but because I can't understand what it could be used for, it didn't help me much...
Any idea ?
Here is my test code (spec/requests/planet_pages_spec.rb)
describe "Create planet" do
before do
visit new_planet_path
fill_in "Name", with: "MyPlanet"
click_button "Validate"
end
it {should have_selector('h1', text: "Planet")}
end
When click on "Validate", it leads to the PlanetsController, which calls the "generate_coordinate" method
def create
#planet = Planet.new(name: params[:planet][:name],
coordinates: generate_coordinates, [...])
if #planet.save
redirect_to action: 'index'
else
render 'new'
end
And here is the generate_coordinate method, which seems never been called by rspec (whereas it is when I navigate through with my browser)
module PlanetsHelper
def generate_coordinates
coordinates = "0.0.0.0"
end
end
If your generate_coordinate method is used by both your controller and helper, consider moving into your controller (as a private method) and adding this one-liner to allow views and helpers to access it:
# planets_controller.rb
helper_method :generate_coordinate
helper_method exposes controller methods to views and helpers within the scope of the controller (in this case, planets#index, planets#show, etc).
If you'd rather do it the other way round you have two options:
insert include PlanetsHelper at the top of the controller (under class PlanetsController)
when you want to call the helper method, call it like this: view_context.generate_coordinate(...)
Try them out and see which one suits your needs best.