Rspec custom failure message error when match runs matchers - ruby-on-rails

I am using Steak to do acceptance testing because I didn't like cucumber at all although I am using some cucumber concepts in the way I test. I liked the declarative vs imperative styles for testings and I am abstracting some expectations into elaborated custom rspec matchers that insinde the match method use other matchers, heres an example:
RSpec::Matchers.define :show_post do |post|
match do |page|
within '.post' do
page.should have_content post.title
page.should have_content post.tagline
page.should have_content post.body
page.should list_author post.author
end
end
end
The only problem I am having is that if my matcher fails I get a generic message that doesn't give me any insight on what's missing, when what I really want is to now which one of the expectation that compose the custom matcher is not meet.
I've been living with this nuisance for a while because I really like the expressiveness of being able to do:
page.should show_post Post.last

Got it:
class ShowsPost
include Capybara::RSpecMatchers
def initialize post
#post = post
end
def matches? page
page.should have_content #post.title
page.should have_content #post.tagline
page.should have_content #post.body
page.should list_author #post.author
end
end
def show_post post
ShowsPost.new(post)
end

Better yet:
module DefineMatcher
def define_matcher name, &block
klass = Class.new do
include Capybara::RSpecMatchers
attr_reader :expected
def initialize expected
#expected = expected
end
define_method :matches?, &block
end
define_method name do |expected|
klass.new expected
end
end
end
module PageMatchers
extend DefineMatcher
define_matcher :show_notice do |page|
within '.alert-notice' do
page.should have_content expected
end
end
end
RSpec.configuration.include PageMatchers, :type => :acceptance

Related

Testing will paginate gem with Rspec/Capybara

In my application there is an admin part, which is restricted to superadmins (users with a property superadmin: true). I've got a shop list, which I want to get paginated and tested.
When debugging the current code with save_and_open_page I get a blank page. If I log in not as a superadmin, I get redirected to application's root and when trying to debug with save_and_open_page is see the root page.. If I do not log in at all, then I'll get redirected to the sign in page. So the basic functionality should work.
I'm having no clue why it does not work with superadmin and why I do not see the shops list when debugging with save_and_open_page.
This is my spec/controllers/shops_controller_spec.rb (copied basically from here) :
require 'rails_helper'
RSpec.describe Admin::ShopsController, type: :controller do
context "GET methods" do
describe "#index action" do
before(:all) {
amount = Rails.application.config.page_size
amount.times { FactoryGirl.create(:shop) }
}
before(:each) {
login_as(FactoryGirl.create(:user, superadmin: true), :scope => :user)
}
context "with entries == config.page_size" do
it "has no second page" do
get :index
expect(response).not_to have_selector("a", :href => "/shops?page=2", :content => "2")
# visit admin_shops_path
# expect(page).to have_no_xpath("//*[#class='pagination']//a[text()='2']")
end
end
context "with entries > config.page_size" do
before { FactoryGirl.create(:shop) }
it "has a second page with too many entries" do
visit "/admin/shops"
save_and_open_page
expect(page).to have_xpath("//*[#class='pagination']//a[text()='2']")
end
it "correctly redirects to next page" do
visit admin_shops_path
find("//*[#class='pagination']//a[text()='2']").click
expect(page.status_code).to eq(200)
end
end
end
end
end
As you can see, I tried to test in different ways (the "expect block" is taken from this SO-question), but none of them work. Using get :index I receive
Admin::ShopsController GET methods #index action with entries == config.page_size has no second page
Failure/Error: expect(page).not_to have_selector("a", :href => "/shops?page=2", :content => "2")
ArgumentError:
invalid keys :href, :content, should be one of :count, :minimum, :maximum, :between, :text, :id, :class, :visible, :exact, :exact_text, :match, :wait, :filter_set
Here is my AdminController.rb if it helps:
class AdminController < ApplicationController
layout 'admin'
before_action :authenticate_user!, :verify_is_superadmin
before_action :set_locale
before_action :get_breadcrumbs
private
def get_breadcrumbs
splitted_url = request.original_fullpath.split("/")
# Remove first object
splitted_url.shift
result = splitted_url.map { |element| element.humanize.capitalize }
session[:breadcrumbs] = result
# debug
end
def set_locale
I18n.locale = params[:locale] || session[:locale] || I18n.default_locale
# session[:locale] = I18n.locale
end
def verify_is_superadmin
(current_user.nil?) ? redirect_to(root_path) : (redirect_to(root_path) unless current_user.superadmin?)
end
end
Update
Using Thomas' answer I ended up putting my code in spec/features and it looks like this right now:
require "rails_helper"
RSpec.feature "Widget management", :type => :feature do
before(:each) {
amount = Rails.application.config.page_size
amount.times { FactoryGirl.create(:shop) }
}
before(:each) {
login_as(FactoryGirl.create(:user, superadmin: true), :scope => :user)
}
scenario "with entries == config.page_size" do
visit admin_shops_path
#save_and_open_page
expect(page).to have_no_xpath("//*[#class='pagination']//a[text()='2']")
end
scenario "with entries > config.page_size" do
FactoryGirl.create(:shop)
visit admin_shops_path
expect(page).to have_xpath("//*[#class='pagination']//a[text()='2']")
end
scenario "with entries > config.page_size it correctly redirects to next page" do
FactoryGirl.create(:shop)
visit admin_shops_path
find("//*[#class='pagination']//a[text()='2']").click
expect(page.status_code).to eq(200)
end
end
Everything works!
You've got a number of issues here.
Firstly the other SO question you linked to isn't using Capybara so copying its examples for matchers is wrong.
Secondly you are writing controller tests, not view tests or feature tests. controller tests don't render the page by default, so to test elements on the page you want to be writing either view tests or feature tests. Capybara is designed for feature tests and isn't designed for controller tests. This is why the default capybara/rspec configuration file only includes the Capybara DSL into tests of type 'feature'. It also includes the Capybara RSpec matchers into view tests since they are useful with the rendered strings provided there.
Thirdly, you are mixing usage of get/response, and visit/page in the same file which just confuses things.
If you rewrite these as feature tests, then to check you don't have a link with a specific href in capybara you would do
expect(page).not_to have_link(href: '...')
If you want to make sure that a link doesn't exist with specific text and a specific href
expect(page).not_to have_link('link text', href: '...')
Note: that checks there is not a link with both the given text and the given href, there could still be links with the text or the href

RSpec before in a helper

Is it possible to do something like this?
module MyHelper
before (:each) do
allow(Class).to receive(:method).and_return(true)
end
end
Then in my tests I could do something like:
RSpec.describe 'My cool test' do
include MyHelper
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
EDIT: This produces the following error:
undefined method `before' for MyHelper:Module (NoMethodError)
Essentially I have a case where many tests do different things, but a common model across off of them reacts on an after_commit which ends up always calling a method which talks to an API. I dont want to GLOBALLY allow Class to receive :method as, sometimes, I need to define it myself for special cases... but I'd like to not have to repeat my allow/receive/and_return and instead wrap it in a common helper...
You can create a hook that is triggered via metadata, for example :type => :api:
RSpec.configure do |c|
c.before(:each, :type => :api) do
allow(Class).to receive(:method).and_return(true)
end
end
And in your spec:
RSpec.describe 'My cool test', :type => :api do
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
You can also pass :type => :api to individual it blocks.
It is possible to do things like you want with feature called shared_context
You could create the shared file with code like this
shared_file.rb
shared_context "stubbing :method on Class" do
before { allow(Class).to receive(:method).and_return(true) }
end
Then you could include that context in the files you needed in the blocks you wanted like so
your_spec_file.rb
require 'rails_helper'
require 'shared_file'
RSpec.describe 'My cool test' do
include_context "stubbing :method on Class"
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
And it will be more naturally for RSpec than your included/extended module helpers. It would be "RSpec way" let's say.
You could separate that code into shared_context and include it into example groups (not examples) like this:
RSpec.describe 'My cool test' do
shared_context 'class stub' do
before (:each) do
allow(Class).to receive(:method).and_return(true)
end
end
describe "here I am using it" do
include_context 'class stub'
it 'Tests a Class Method' do
expect { Class.method }.to eq true
end
end
describe "here I am not" do
it 'Tests a Class Method' do
expect { Class.method }.not_to eq true
end
end
end
Shared context can contain let, helper functions & everything you need except examples.
https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-context

Capybara method is unacceptable from the calss undefined method `expect' for #<ArticleForm:0xb5e98bc>

The method "expect" it not accessible when trying to call from a class method. However, it works fine when calling from the feature spec.So basically the last method (i_expect_to_see_article_on_home_page) is not working.
spec/support/ArticleForm.rb
require 'rails_helper'
class ArticleForm
include Capybara::DSL
def visit_home_page
visit('/')
self
end
def create_an_article
click_on('New Article')
fill_in('Title', with: "My title")
fill_in('Content', with: "My content")
click_on('Create Article')
self
end
def i_expect_to_see_article_on_home_page
visit('/')
expect(page).to have_text("My title")
expect(page).to have_text("My content")
self
end
end
spec/features/article_spec.rb
require 'rails_helper'
require_relative '../support/ArticelForm.rb'
feature "home page" do
article_form = ArticleForm.new
scenario "Visit the home page and post an article" do
article_form.visit_home_page.create_an_article
article_form.visit_home_page.i_expect_to_see_article_on_home_page
end
end
You need to include RSpec::Matchers in your object. You will probably also need to do the same with Capybara::RSpecMatchers if you want to use the capybara matchers
Im not familiar with the syntax you're using but I believe you need to wrap it into "it block".This is how I normally write it:
describe "i_expect_to_see_article_on_home_page" do
it 'should do something' do
visit('/')
expect(page).to have_text("My title")
expect(page).to have_text("My content")
end
end

Does Rubymine support "let" syntax in rails?

I am using Rubymine to create a project in Rails4, rspec and capybara. When I use the let syntax for defining variables in Capybara features, it seems RubyMine isn't able to detect the existence of the variables. For instance in this code below, the variable capsuleHash, capsuleForm and capsuleViewPage are all not being recognized in intelliJ in the scenario section. Does anyone have a workaround?
require 'spec_helper'
feature 'Capsules Feature' do
let(:capsuleHash) {attributes_for(:tdd_capsule)}
let(:capsuleForm) {CapsuleCreateForm.new}
let(:capsuleViewPage) {CapsuleViewPage.new}
scenario 'Add a new capsule and displays the capsule in view mode' do
visit '/capsules/new'
expect{
capsuleForm.submit_form(capsuleHash)
}.to change(Capsule,:count).by(1)
capsuleViewPage.validate_on_page
expect(page).to have_content capsuleHash[:title]
expect(page).to have_content capsuleHash[:description]
expect(page).to have_content capsuleHash[:study_text]
expect(page).to have_content capsuleHash[:assignment_instructions]
expect(page).to have_content capsuleHash[:guidelines_for_evaluators]
expect(page).to have_link 'Edit'
end
end
I'm not familiar with RubyMine other than it's an IDE for Ruby. The way you phrased your question, though, I'm assuming that you're referring to some feature of RubyMine which displays the "variables" defined at any particular point in a program.
If this is case, the reason that the symbols you've passed to let wouldn't "show up" as variables is because they are not being defined as variables. They are being defined as methods which return the value of the associated block. On the first call from within each it block, the value of the block is remembered and that value returned on subsequent calls within the same block.
Note that there is nothing wrong with the RSpec code in terms of defining those methods. The following code passes, for example:
class Page
def has_content?(content) true ; end
def has_link?(link) true ; end
end
page = Page.new
class CapsuleCreateForm
def submit_form(hash)
Capsule.increment_count
end
end
class CapsuleViewPage
def validate_on_page
end
end
def attributes_for(symbol)
{}
end
def visit(path)
end
class Capsule
##count = 0
def self.count
##count
end
def self.increment_count
##count += 1
end
end
describe 'Capsules Feature' do
let(:capsuleHash) {attributes_for(:tdd_capsule)}
let(:capsuleForm) {CapsuleCreateForm.new}
let(:capsuleViewPage) {CapsuleViewPage.new}
it 'Add a new capsule and displays the capsule in view mode' do
visit '/capsules/new'
puts method(:capsuleHash)
expect{
capsuleForm.submit_form(capsuleHash)
}.to change(Capsule,:count).by(1)
capsuleViewPage.validate_on_page
expect(page).to have_content capsuleHash[:title]
expect(page).to have_content capsuleHash[:description]
expect(page).to have_content capsuleHash[:study_text]
expect(page).to have_content capsuleHash[:assignment_instructions]
expect(page).to have_content capsuleHash[:guidelines_for_evaluators]
expect(page).to have_link 'Edit'
end
end
RubyMine does support let blocks, but you'll need to be sure to use the latest version, 6.0.2. See http://youtrack.jetbrains.com/issue/RUBY-14673

Reusing minitest integration tests

I have three pages in Rails that all display the same header, and hence would require the exact same integration tests.
Instead of repeating myself and writing separate tests that look almost exactly the same, what's the best approach here? I've tried putting the shared assertions into a module but haven't been successful getting it to load into each test scenario.
UNDRY:
class IntegrationTest
describe "page one" do
before { visit page_one_path }
it "should have a home page link" do
page.find_link "Home"
end
end
describe "page two" do
before { visit page_two_path }
it "should have a home page link" do
page.find_link "Home"
end
end
describe "page three" do
before { visit page_three_path }
it "should have a home page link" do
page.find_link "Home"
end
end
end
Failed attempt at drying it out...
Module:
/lib/tests/shared_test.rb
module SharedTest
def test_header
it "should have a home page link" do
page.find_link "Home"
end
end
end
Test:
class IntegrationTest
include SharedTest
describe "page one" do
before { visit page_one_path }
test_header
end
describe "page two" do
before { visit page_two_path }
test_header
end
describe "page three" do
before { visit page_three_path }
test_header
end
end
I haven't quite figured out how to write modules yet so it's no surprise that this doesn't work. Can someone point me in the right direction?
The way to share tests between different describe blocks when using Minitest's spec DSL is to include the module in each describe block you want those tests to run in.
module SharedTest
def test_header
assert_link "Home"
end
end
class IntegrationTest < ActiveDispatch::IntegrationTest
describe "page one" do
include SharedTest
before { visit page_one_path }
end
describe "page two" do
include SharedTest
before { visit page_two_path }
end
describe "page three" do
include SharedTest
before { visit page_three_path }
end
end
One of the ways the Minitest's Test API is different than Minitest Spec DSL is in how they behave when being inherited. Consider the following:
class PageOneTest < ActiveDispatch::IntegrationTest
def setup
visit page_one_path
end
def test_header
assert_link "Home"
end
end
class PageTwoTest < PageOneTest
def setup
visit page_two_path
end
end
class PageThreeTest < PageOneTest
def setup
visit page_three_path
end
end
The PageTwoTest and PageThreeTest test classes inherit from PageOneTest, and because of that they all have the test_header method. Minitest will run all three tests. But, when implemented with the spec DSL the test_header method is not inherited.
class PageOneTest < ActiveDispatch::IntegrationTest
def setup
visit page_one_path
end
def test_header
assert_link "Home"
end
describe "page two" do
before { visit page_two_path }
end
describe "page three" do
before { visit page_three_path }
end
end
In this case, only one test is run. The test class created by describe "page two" will inherit from PageOneTest, but will have all of the test methods removed. Why? Because Minitest's spec DSL is based on RSpec, and this is the way that RSpec works. Minitest goes out of its way to nuke the test methods that are inherited when using the spec DSL. So the only way to share tests while using the spec DSL is to include the module in each describe block you want them to be in. All other non-test methods, including the before and after hooks, and the let accessors, will be inherited
Here's a clean way to reuse tests when using MiniTest's spec DSL - define the tests inside a function and call that function where you want to include your tests.
Example:
def include_shared_header_tests
it "should be longer than 5 characters" do
assert subject.length > 5
end
it "should have at least one capital letter" do
assert /[A-Z]/ =~ subject
end
end
# This block will pass
describe "A good header" end
subject { "I'm a great header!" }
include_shared_header_tests
end
# This block will fail
describe "A bad header" end
subject { "bad" }
include_shared_header_tests
end
If you want to continue to use the Spec style in the module, you can use the Module::included hook.
module SharedTests
def self.included spec
spec.class_eval do
# Your shared code here...
let(:foo) { 'foo' }
describe '#foo' do
it { foo.must_equal 'foo' }
end
end
end
end
class MyTest < Minitest::Spec
include SharedTests
end
class MyOtherTest < Minitest::Spec
include SharedTests
end
# It also works in nested describe blocks.
class YetAnotherTest < Minitest::Spec
describe 'something' do
describe 'when it acts some way' do
include SharedTests
end
end
end
Another approach to repetitive tests is iteration.
class IntegrationTest < Minitest::Spec
{
'page one' => :page_one_path,
'page two' => :page_two_path,
'page three' => :page_three_path,
}.each do |title, path|
describe title do
before { visit send(path) }
it 'should have a home page link' do
page.find_link 'Home'
end
end
end
end

Resources