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.
Related
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'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!
I am using rspec-rails and I want to test that my mailer is rendering the correct view template.
describe MyMailer do
describe '#notify_customer' do
it 'sends a notification' do
# fire
email = MyMailer.notify_customer.deliver
expect(ActionMailer::Base.deliveries).not_to be_empty
expect(email.from).to include "cs#mycompany.com"
# I would like to test here something like
# ***** HOW ? *****
expect(template_path).to eq("mailers/my_mailer/notify_customer")
end
end
end
Is this a valid approach? Or shall I do something completely different to that?
Update
MyMailer#notify_customer might have some logic (e.g. depending on the locale of the customer) to choose different template under different circumstances. It is more or less similar problem with controllers rendering different view templates under different circumstances. With RSpec you can write
expect(response).to render_template "....."
and it works. I am looking for something similar for the mailers.
I think this is a step closer to the answer above, since it does test for implicit templates.
# IMPORTANT!
# must copy https://gitlab.com/gitlab-org/gitlab/-/blob/master/spec/support/helpers/next_instance_of.rb
it 'renders foo_mail' do
allow_next_instance_of(described_class) do |mailer|
allow(mailer).to receive(:render_to_body).and_wrap_original do |m, options|
expect(options[:template]).to eq('foo_mail')
m.call(options)
end
end
body = subject.body.encoded
end
OK, I understand what you're trying to achieve now.
You should be able to test which template is called by setting expectations on your mailer for the mail method having been called with particular arguments.
Try this in your test:
MyMailer.should_receive(:mail).with(hash_including(:template => 'expected_template'))
I'm writing integration tests using Rspec and Capybara. I've noticed that quite often I have to execute the same bits of code when it comes to testing the creation of activerecord options.
For instance:
it "should create a new instance" do
# I create an instance here
end
it "should do something based on a new instance" do
# I create an instance here
# I click into the record and add a sub record, or something else
end
The problem seems to be that ActiveRecord objects aren't persisted across tests, however Capybara by default maintains the same session in a spec (weirdness).
I could mock these records, but since this is an integration test and some of these records are pretty complicated (they have image attachments and whatnot) it's much simpler to use Capybara and fill out the user-facing forms.
I've tried defining a function that creates a new record, but that doesn't feel right for some reason. What's the best practice for this?
There are a couple different ways to go here. First of all, in both cases, you can group your example blocks under either a describe or context block, like this:
describe "your instance" do
it "..." do
# do stuff here
end
it "..." do
# do other stuff here
end
end
Then, within the describe or context block, you can set up state that can be used in all the examples, like this:
describe "your instance" do
# run before each example block under the describe block
before(:each) do
# I create an instance here
end
it "creates a new instance" do
# do stuff here
end
it "do something based on a new instance" do
# do other stuff here
end
end
As an alternative to the before(:each) block, you can also use let helper, which I find a little more readable. You can see more about it here.
The very best practice for your requirements is to use Factory Girl for creating records from a blueprint which define common attributes and database_cleaner to clean database across different tests/specs.
And never keep state (such as created records) across different specs, it will lead to dependent specs. You could spot this kind of dependencies using the --order rand option of rspec. If your specs fails randomly you have this kind of issue.
Given the title (...reusing code in Rspec) I suggest the reading of RSpec custom matchers in the "Ruby on Rails Tutorial".
Michael Hartl suggests two solutions to duplication in specs:
Define helper methods for common operations (e.g. log in a user)
Define custom matchers
Use these stuff help decoupling the tests from the implementation.
In addition to these I suggest (as Fabio said) to use FactoryGirl.
You could check my sample rails project. You could find there: https://github.com/lucassus/locomotive
how to use factory_girl
some examples of custom matchers and macros (in spec/support)
how to use shared_examples
and finally how to use very nice shoulda-macros
I would use a combination of factory_girl and Rspec's let method:
describe User do
let(:user) { create :user } # 'create' is a factory_girl method, that will save a new user in the test database
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
end
# spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
username { Faker::Internet.user_name }
end
end
This allows you to do great stuff like this:
describe User do
let(:user) { create :user, attributes }
let(:attributes) { Hash.new }
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
context "when user is admin" do
let(:attributes) { { admin: true } }
it "should be able to walk" do
user.walk.should be_true
end
end
end
I'm very rigorous when it comes to my HTML markup and I follow a strict coding convention for forms, lists, etc...
I would like to include reusable test in my RSpec tests that would allow for me call a form test from any other test and target it directly to the page or URL that I'm testing.
Something like this:
# spec/helpers/form_tester.rb
describe FormTester
it "should check to see if all the text fields have an ID prefix of 'input-'" do
... #form should be valid ...
should be true
end
end
# spec/requests/user_form.rb
describe UserForm
it "should validate the form" do
#form = find(:tag,'form')
# call the FormTester method
end
end
Any ideas on how todo this? I'm using Rails 3.1, with RSpec, Capybara and FactoryGirl.
Use shared examples. In you case, something like this may work:
# spec/shared_examples_for_form.rb
shared_examples 'a form' do
describe 'validation' do
it 'should be validated' do
form.should_be valid
end
end
end
# spec/requests/user_form.rb
describe UserForm
it_behaves_like 'a form' do
let(:form) { find(:tag, 'form') }
end
end
It's also possible to pass parameters to shared examples and place the shared examples inside spec/support. Just have a read at the documentation.
Shared examples are great, but you've got at least two serious problems here.
First: why are you giving your form fields IDs at all? You've already got perfectly good selectors: just use input[name='whatever']. And even if you are giving them IDs, don't put a prefix on them: input#whatever or just #whatever is probably a more sensible selector in your CSS than #input-whatever. By being overspecific on your selector names, you're most likely making your CSS and JavaScript harder to write than they have to be.
Second: don't use RSpec to test your views. RSpec is at its best when confined to models. Cucumber is better for anything user-facing.