Using Rspec and Capybara for feature testing. Unfortunately, I'm running into problems...
basic_interaction_spec.rb
RSpec.describe "basic interaction" do
before :each do
category = build_stubbed(:category, name: "Pants")
end
it "displays category" do
visit("/")
click_link("Pants")
expect(current_path).to eq("pants")
expect(page).to have_title("Pants | app_name")
end
end
Results in
Failure/Error: <li><%= link_to category.name, products_path(category_or_product: category.slug) %></li>
ActionView::Template::Error:
undefined method `name' for nil:NilClass
homepage_controller.rb
def index
#categories = []
Category.root_order.each do |category_name|
#categories << Category.find_by(name: category_name)
end
Can you guys see where I've gone wrong?
When writing feature specs you can't use build_stubbed for records you want your app to be able to access. Assuming the category you're building in the before block is what you're expecting the app to display on the page, you need to actually create the record because the app is accessing it via a DB query.
before :each do
category = create(:category, name: "Pants")
end
Beyond that, you should never be using basic RSpec matchers (eq, etc) with Capybara objects, instead you should be using the Capybara provided matchers which take care of the asynchronous nature of tests using a browser by providing waiting/retrying behavior. So instead of
expect(current_path).to eq("pants")
you should have something like
expect(page).to have_current_path("pants")
Figured out why the problem occurred.
Forgot about the method in the category model which ensures that only the top-level categories are displayed on the homepage.
def self.root_order
%w[Tops Outerwear Pants Suits Shoes]
end
This caused a problem when not all top categories were created. With the following fixtures the test pass.
before :each do
category1 = create(:category, name: "Tops")
category2 = create(:category, name: "Outerwear")
category3 = create(:category, name: "Pants")
category4 = create(:category, name: "Suits")
category5 = create(:category, name: "Shoes")
end
Related
So I was looking at: https://rubyplus.com/articles/1491-Basic-TDD-in-Rails-Writing-Validation-Tests-for-the-Model
Just seeing techniques of testing and I saw this:
require 'rails_helper'
describe Article, type: :model do
it 'is valid if title and description fields have value' do
expect do
article = Article.new(title: 'test', description: 'test')
article.save
end.to change{Article.count}.by(1)
end
end
Specifically the last line: end.to change{Article.count}.by(1). From reading https://relishapp.com/rspec/rspec-expectations/v/3-7/docs/built-in-matchers/change-matcher
It says specifically:
The change matcher is used to specify that a block of code changes
some mutable state. You can specify what will change using either of
two forms:
Which makes sense. But were testing Article.count in the block of code which isn't actually "doing" anything (The article.save is what actually changed the Article.count so how exactly does this work? Does the test take a a look at whats in the block of code before it's ran and "prerun" it...the compare the .by(1) after?
Thanks
There are two blocks of code being executed. The block of code passed to expect, and the block of code passed to change. This is what's really happening, in pseudo-code.
difference = 1
initial_count = Article.count
article = Article.new(title: 'test', description: 'test')
article.save
final_count = Article.count
expect(final_count - initial_count).to eq(difference)
I would refactor your test to be a little easier to follow as this:
require 'rails_helper'
describe Article, type: :model do
let(:create_article) { Article.create(title: 'test', description: 'test') }
it 'is valid if title and description fields have value' do
expect { create_article }.to change { Article.count }.by(1)
end
end
Goal: Trying to submit empty register form fails due to validation errors. Expects are present in scenario as single line (method). Errors are being checked very precisely in terms of their placement. I don't want to use classical counting until that's the only way left.
def cant_be_blank_error_is_displayed(number_of_times = 1)
expect(page).to have_content t('errors.messages.blank'), count: number_of_times
end
require 'rails_helper'
require 'email_helper'
feature 'register organisation', type: :feature, js: true do
let!(:register_page) { RegisterPage.new }
let!(:login_page) { LoginPage.new }
let!(:organisation) { create :organisation, name: 'organisation_name' }
let!(:owner) { create :user, email: 'user#example.com', role: :owner }
let(:form) { register_page.register_form }
before { register_page.load }
context 'creation fails because' do
scenario 'mandatory fields were left empty' do
register_page.submit_empty_form
all_mandatory_fields_cant_be_blank_error_displayed
# expect(form).to have_content mail_blank_or_taken_error
end
scenario (...)
end
end
Method:
1. Define specific errors as elements in the page
class RegisterPage < SitePrism::Page
set_url "/sign_up"
section :register_form, :xpath, "//*[#id='new_user']" do
element :email, :xpath, "//*[#id='user_email']"
(...) # other input fields
# binding.pry confirms they appear on the page
element :mail_blank_or_taken_error, :xpath, "//*[#id='new_user']/div[2]/span"
element :blank_password_error, :xpath, "//*[#id='new_user']/div[4]/span"
element :password_too_short_error, :xpath, "//*[#id='new_user']/div[2]/div[3]/p"
element :mismatch_error, :xpath, "//*[#id='new_user']/div[2]/div[4]/span"
element :blank_name_error, :xpath, "//*[#id='new_user']/div[2]/div[5]/span"
element :name_taken_error, :xpath, "//*[#id='new_user']/div[2]/div[5]"
element :wrong_format_error, :xpath, "//*[#id='new_user']/div[7]/span
(...)
end
end
2. Create error_alerts.rb helper to store expects in one method
def all_mandatory_fields_cant_be_blank_error_displayed
[
mail_blank_or_taken_error,
blank_password_error,
signs_limit_error,
blank_name_error,
blank_adress_line_error,
blank_city_error,
blank_country_error,
blank_zipcode_error,
blank_phone_number_error,
blank_website_error,
].each do |validation_errors|
expect(form).to have_content validation_errors
end
end
Obstacle:
Errors are default rails ones, so they are not defined as translations, which I used in another spec in the same convention and everything worked flawlessly.
example:
def invalid_email_or_password_error_displayed
expect(login_page).to have_content t("devise.failure.invalid", authentication_keys: "Email")
end
Failures: I get following errors.
1# my way - same issue occurs for each object from the |validation_errors|
1) register organisation creation fails because mandatory fields were left empty
Failure/Error: mail_blank_or_taken_error,
NameError:
undefined local variable or method `mail_blank_or_taken_error' for #<RSpec::ExampleGroups::RegisterOrganisation::CreationFailsBecause:0x007fe56b336370>
# ./spec/support/error_alerts.rb:63:in `all_mandatory_fields_cant_be_blank_error_displayed'
# ./spec/features/organisation_registration_spec.rb:16:in `block (3 levels) in <top (required)>'
2# classical expect
1) register organisation creation fails because mandatory fields were left empty
Failure/Error: expect(form).to have_content
mail_blank_or_taken_error
NameError:
undefined local variable or method `mail_blank_or_taken_error' for # <RSpec::ExampleGroups::RegisterOrganisation: :CreationFailsBecause:0x007ff02e7b9348>#./spec/features/organisation_registration_spec.rb:17:in `block (3 levels) in <top (required)>'
Question: Is my way technically achievable after some modifications or the only solution is to use classical counting method? Can loop iterate through page's elements? Here's an example of its similiar use, though expects are strings and the loop itself is included in spec's scenario, instead of being exported to external method in a helper.
scenario "is successful" do
all_ministry_memberships_page.create_new(ministry, cabinet, minister)
[
t("flash.actions.create.notice", resource_name: "Ministry membership"),
"Ministry " + ministry.name,
"Cabinet " + cabinet.name,
"Minister " + minister.name,
"Start Date " + (cabinet.appointment_date + 2.days).strftime("%Y-%m-%d"),
"Created At " + ministry_membership.created_at.strftime("%B %0e, %Y %H:%M"),
"Updated At " + ministry_membership.updated_at.strftime("%B %0e, %Y %H:%M"),
].each do |form_input|
expect(form).to have_content form_input
end
end
This
[
mail_blank_or_taken_error,
blank_password_error,
]
means "call methods mail_blank_or_taken_error(), then blank_password_error() and put their results in an array". You don't have such methods, and that's what the error message tells you. If you wanted those to be strings, make them strings.
[
"mail_blank_or_taken_error",
"blank_password_error",
]
or
%w[
mail_blank_or_taken_error
blank_password_error
]
I found solution to this problem.
The method needed proper argument parsed and page_object reference
def all_mandatory_fields_cant_be_blank_error_displayed_in(form)
[
form.blank_password_error,
form.signs_limit_error,
form.blank_name_error,
form.blank_adress_line_error,
form.blank_city_error,
form.blank_country_error,
form.blank_zipcode_error,
form.blank_phone_number_error,
form.blank_website_error,
].each do |validation_error|
expect(validation_error).to be_present
end
end
I have the following ROR RSpec test:
Keep in mind that the test does pass as is in the code below. The method is correctly defined and does what is intended. The question is why when I modify and remove the [] around the #public_topic in the second example the test fails?
describe "scopes" do
before do
#public_topic = Topic.create!(name: RandomData.random_sentence, description: RandomData.random_paragraph)
#private_topic = Topic.create!(name: RandomData.random_sentence, description: RandomData.random_paragraph, public: false)
end
describe "visible_to(user)" do
it "returns all topics if user is present" do
user = User.new
expect(Topic.visible_to(user)).to eq(Topic.all)
end
it "returns only public topics if user is nil" do
expect(Topic.visible_to(nil)).to eq([#public_topic])
end
end
end
update
scope :visible_to, -> { where(public: true) }
It is hard to say without seeing the implementation of visible_to.
From the first example, it looks like that method returns an ActiveRecord::Relation object. That is going to represent a collection of objects and not a single object.
So, in essence, it comes down to:
object != [object]
I am using Rails 4 and Ruby 2.2 with RSPEC, Capybara and FactoryGirl.
I am trying to test that a user can write a story adding characters (other users). It works in the browser, but when I run the tests, I get the following message, indicating that the title is missing:
Failure/Error: expect(page).to have_content("Thank you for sharing a story.")
expected to find text "Thank you for sharing a story." in "Family Matters Write New Story Add User Log Out * Title DescriptionSo this is what happened that night in 1972 Who was a part of this story?Loni Cabral Fernando Cabral Cover image"
When I add save_and_open_page, I can see that the title has been inserted. When I remove the lines to select characters, the tests pass.
Here is the test file:
require 'rails_helper'
require_relative '../support/new_story_form'
feature 'create story' do
let(:new_story_form) { NewStoryForm.new}
let(:user) { FactoryGirl.create(:active_user) }
let(:character1) { FactoryGirl.create(:active_user, first_name:"Loni", last_name:"Cabral") }
let(:character2) { FactoryGirl.create(:active_user, first_name:"Fernando", last_name:"Cabral") }
before do
login(user)
user.add_relative(character1, "Child")
user.add_relative(character2, "Child")
end
scenario 'create new story with valid data' do
new_story_form.visit_form.fill_in_with(
title: "Great story",
cover_image: "cover_image.png"
).submit
expect(page).to have_content("Thank you for sharing a story.")
expect(page).to have_content("Who was involved:")
expect(page).to have_content(character1.name)
expect(page).to have_content(character2.name)
expect(Story.last.cover_image_identifier).to eq("cover_image.png")
expect(Story.last.title).to eq("Great story")
expect(Story.last.user).to eq(user)
expect(Story.last.participants.first).to eq(character1)
end
scenario 'cannot create story with invalid data' do
new_story_form.visit_form.submit
expect(page).to have_content(" can't be blank")
end
end
And here is the new_story_form support file:
class NewStoryForm
include Capybara::DSL
def visit_form
visit('/')
click_on("Write New Story")
self
end
def fill_in_with(params = {})
fill_in("Title", with: params.fetch(:title, "Great story"))
fill_in("Description", with: "So this is what happened that night in 1972")
attach_file('Cover image', "#{Rails.root}/spec/fixtures/" + params.fetch(:cover_image, "cover_image.png"))
select("Loni Cabral", from:"Who was a part of this story?")
select("Fernando Cabral", from:"Who was a part of this story?")
self
end
def submit
click_on("Create Story")
end
end
Edit
After much debugging, I realized that the tests are failing because of a validation that is not working correctly.
It looks like your submit is failing and so it's redirecting back to the edit page, and not showing the "Thank you for sharing a story." text - Since you say it works if you don't select the characters I would guess it's some kind of nested attributes error, but your test log should explain exactly why the submit is failing. The reason the title isn't shown in the text searched is because the value of an <input type="text"/> element (which is being shown because it's redirecting to the edit page) is not part of the text content of a page
Here is my helper method which I want to test.
def posts_correlation(name)
if name.present?
author = User.find_by_name(name)
author.posts.count * 100 / Post.count if author
end
end
A factory for user.
factory :user do
email 'user#example.com'
password 'secret'
password_confirmation { password }
name 'Brian'
end
And finally a test which permanently fails.
test "should calculate posts count correlation" do
#author = FactoryGirl.create(:user, name: 'Jason')
#author.posts.expects(:count).returns(40)
Post.expects(:count).returns(100)
assert_equal 40, posts_correlation('Jason')
end
Like this.
UsersHelperTest:
FAIL should calculate posts count correlation (0.42s)
<40> expected but was <0>.
test/unit/helpers/users_helper_test.rb:11:in `block in <class:UsersHelperTest>'
And the whole problem is that mocha doesn't really mock the count value of author's posts, and it returns 0 instead of 40.
Are there any better ways of doing this: #author.posts.expects(:count).returns(40) ?
When your helper method runs, it's retrieving its own object reference to your author, not the #author defined in the test. If you were to puts #author.object_id and puts author.object_id in the helper method, you would see this problem.
A better way is to pass the setup data for the author in to your mocked record as opposed to setting up expectations on the test object.
It's been a while since I used FactoryGirl, but I think something like this should work:
#author = FactoryGirl.create(:user, name: 'Jason')
(1..40).each { |i| FactoryGirl.create(:post, user_id: #author.id ) }
Not terribly efficient, but should at least get the desired result in that the data will actually be attached to the record.