I have a spec like this:
context 'index' do
let!(:article) { create :article }
subject { visit articles_path }
specify do
subject
expect(page).to have_content(article.title)
end
end
However when I try to refactor it like this, it says I have 0 examples:
context 'index' do
let!(:article) { create :article }
subject { visit articles_path }
context do
after { expect(page).to have_content(article.title) }
end
context do
before { login_as :user }
after { expect(page).to have_content(article.comment) }
end
context do
after {}
end
end
Shouldn't it be running subject and then the after hook? I am pretty sure I have used this setup before.
Please tell me how to correctly refactor this. Thanks.
Update
Could do it like this, but I don't really like it:
subject do
visit articles_path
page
end
specify do
expect(subject).to have_content(article.title)
end
'after' block is normally used to do something after the test example has been executed. In your second example you have no test executed prior to the 'after' block
I would refactor your code like this
context 'index' do
let!(:article) { create :article }
visit articles_path
expect(page).to_not have_content(category.title)
end
edit
context 'index' do
let!(:article) { create :article }
before do
visit articles_path
end
context do
it "displays article's title" do
expect(page).to have_content(article.title)
end
end
context do
before { login_as :user }
it "displays article's comments" do
expect(page).to have_content(article.comment)
end
end
context do
#another test
end
end
It seems to me from you refactoring attempt that there's a bit of confusion.
First, the reason why you were seeing 0 examples, 0 failures is because there is no subject invocation in your tests. Think about it like this:
subject { visit articles_path }
it 'has a nice title' do
subject
expect(page).to have_content(article.title)
end
it 'has a nice comment' do
subject
expect(page).to have_content(article.comment)
end
In order for your expectations to work you need to call the subject. In fact you could have even avoided using a subject by explicitly writing in your it/specify blocks visit articles_path
it 'has a nice title' do
visit articles_path
expect(page).to have_content(article.title)
end
it 'has a nice comment' do
visit articles_path
expect(page).to have_content(article.comment)
end
Tests that share the same subject can be dried up using subject { ... }.
Second, don't confuse context blocks with specify/it blocks(remember they're aliased).
A context is a way to make your tests more understandable by separating different results for the test.
subject { visit articles_path }
context 'user is logged in' do
it 'displays article's title' do
login_as :user
subject
expect(page).to have_content(article.title)
end
it 'displays article's title' do
login_as :user
subject
expect(page).to have_content(article.comment)
end
end
context 'user not logged in' do
it 'displays article's comments' do
subject
expect(page).to have_content('Log in')
end
end
You can have different expectations, it/specify blocks in the same context.
Use different context to specify different behaviour of the same functionality.
Finally last step. Group shared functionality in a before block. In our example:
subject { visit articles_path }
before do
subject
end
context 'user is logged in' do
before do
login_as :user
end
it 'displays article's title' do
expect(page).to have_content(article.title)
end
it 'displays article's title' do
expect(page).to have_content(article.comment)
end
end
context 'user not logged in' do
it 'displays article's comments' do
expect(page).to have_content('Log in')
end
end
As you can see, the two contexts run the subject but only the first content logs the user in to test the article page whereas the second context don't.
I hope this was useful.
Keep testing and soon it will become a matter of habit and you will be writing tests much easily.
Related
I'm writing some views tests in an application and my tests expects something like:
describe 'form' do
it 'has a search form' do
render
expect(rendered).to have_selector 'form[id=mock_search]'
end
it 'has a name filter' do
render
expect(rendered).to have_selector 'label[for=q_name_cont]', text: 'Nome do simulado'
expect(rendered).to have_selector 'input[id=q_name_cont]'
end
it 'has a submit button' do
render
expect(rendered).to have_selector 'input[type=submit][value="Buscar"][name=commit]'
end
it 'has a reset button' do
render
expect(rendered).to have_selector 'input[type=submit][value="Limpar filtros"]'
end
end
But I have and before(:each) that iterates too many requests on application making my tests spend 25 seconds to run. I've changed to before(:all) and then become to 4 seconds.
Should I still use before(:each)?
Why before(:all) were not recommended?
EDIT: My before iterations:
before(:each) do
#school = build(:school)
#teacher = build(:teacher)
build_list(:mock_with_proccessed_statistics, 2, school: #school, teacher: #teacher)
#mocks = Mock.page(nil)
#q = Mock.ransack
allow(view).to receive(:current_school).and_return(#school)
allow(view).to receive(:format_date) { |date, format| date.strftime(format) }
end
describe 'form' do
#
#let! will create create instance variable school
#inside your test case and assign your school build. Different between
#let! and let is: let! create before every task. You won't need it to put
#on before block and let doesn't do that you have to call it explicitly to
#create that mock and get values..
let!(:school) { build(:school) }
let!(:teacher) { build(:teacher) }
let!(:statistics) { build_list(:mock_with_proccessed_statistics, 2, school: #school, teacher: #teacher) }
let!(:mocks) { Mock.page(nil) }
subject { render }
it 'has a search form' do
expect{ subject }.to have_selector 'form[id=mock_search]'
end
it 'has a name filter' do
expect{ subject }.to have_selector 'label[for=q_name_cont]', text: 'Nome do simulado'
expect{ subject }.to have_selector 'input[id=q_name_cont]'
end
end
before(:each) execute before each test case executes. and before(:all) once before all test cases inside a current context.
I'm new to Ruby, Rails and RSpec, so I'm doing the railstutorial. In Chapter 10, exercise 1, I've implemented a test for micropost pluralization that I'm trying to refactor.
My first try was:
describe "micropost pluralization" do
let(:new_user) { FactoryGirl.create(:user) }
before do
sign_in new_user
visit root_path
end
describe "with no microposts" do
it "should have no microposts" do
expect(page).to have_text("0 microposts")
end
end
describe "with one micropost" do
before do
FactoryGirl.create(:micropost, user: new_user, content: "Lorem ipsum")
visit root_path
end
it "should have one micropost" do
expect(page).to have_text("1 micropost")
end
end
describe "with two microposts" do
before do
FactoryGirl.create(:micropost, user: new_user, content: "Lorem ipsum")
FactoryGirl.create(:micropost, user: new_user, content: "Dolor sit amet")
visit root_path
end
it "should have two microposts" do
expect(page).to have_text("2 microposts")
end
end
end
This clearly violates the DRY principle. As this test case describes a sequence, I tried to refactor by nesting:
describe "micropost pluralization" do
let(:new_user) { FactoryGirl.create(:user) }
before do
sign_in new_user
visit root_path
end
it "should have no microposts" do
expect(page).to have_text("0 microposts")
end
describe "when one micropost is created" do
before do
FactoryGirl.create(:micropost, user: new_user, content: "Lorem ipsum")
visit root_path
end
it "should have one micropost" do
expect(page).to have_text("1 micropost")
end
describe "and then another micropost is created" do
before do
FactoryGirl.create(:micropost, user: new_user, content: "Dolor sit amet")
visit root_path
end
it "should have two microposts" do
expect(page).to have_text("2 microposts")
end
end
end
end
But I'm still repeating the "visit root_path" line, and that nesting looks ugly.
My questions are:
1 - Is there a better way than nesting to describe a sequence in RSpec? I tried mixing the creation of microposts and the assertions and didn't work.
2 - How could I remove the "visit root_path" duplication?
Thanks for reading this far!
You could try using shared_example_groups:
shared_examples_for 'page with n microposts' do |number_of_posts, expected_text|
before do
number_of_posts.times do
FactoryGirl.create(:micropost, user: new_user, content: "Lorem ipsum")
end
visit root_path
end
it "should have correct text" do
expect(page).to have_text(expected_text)
end
end
it_should_behave_like 'page with n microposts', 0, '0 microposts'
it_should_behave_like 'page with n microposts', 1, '1 micropost'
it_should_behave_like 'page with n microposts', 2, '2 microposts'
I would personally make just a helper method to extract my logic for creation of objects.Here is the code and a better solution would be to pull create_microposts in a helper module to be included in your tests.I use context which describe clearly my expectations and that those are just similar tests, connected to each other.
describe "micropost pluralization" do
def create_microposts(microposts)
microposts.each do |micropost|
FactoryGirl.create(:micropost, user: new_user, content: micropost)
end
end
let(:new_user) { FactoryGirl.create(:user) }
before do
sign_in new_user
create_microposts microposts
visit root_path
end
context "when no microposts exist for user" do
let(:microposts) { [] }
it { is_expected.to have_text("0 microposts") }
end
context "when one micropost is created" do
let(:microposts) { ["Lorem ipsum"] }
it { is_expected.to have_text("1 microposts") }
end
context "when two micropost are created" do
let(:microposts) { ["Lorem ipsum", "Dolor sit amet"] }
it { is_expected.to have_text("2 microposts") }
end
end
In order to avoid nesting to describe sequences in RSpec you might want to check out contexts. As was pointed out in the comment to my answer, you cannot remove the duplicity of the visit_root_path due to the fact that the various test cases need different prerequisites (i.e. one needs 1 post to be created, the other needs 2).
I'd write this test suite like so:
describe 'micropost pluralization' do
let :new_user { create :user }
before :all do
sign_in new_user
end
context 'no microposts' do
it 'should have no microposts' do
visit_root_path
expect(page).to have_text('0 microposts')
end
end
context 'one or more microposts' do
# you could have a before :each here do create the posts, but
# since you only have two test cases I don't feel it's necessary.
it 'should have one micropost' do
create :micropost, user: new_user, content: 'Lorem ipsum'
visit_root_path
expect(page).to have_text('1 micropost')
end
it 'should have two microposts' do
2.times.do { create :micropost, user: new_user, content: 'Lorem ipsum' }
visit_root_path
expect(page).to have_text('2 microposts')
end
end # end context
end # end describe
It might not look perfectly DRY, but it certainly looks readable to me.
I cannot recommend betterspecs enough for test writing by the way :-)
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
I am working with rails rspec/capybara/declarative_authorization. I have to run the same test with a lot of different users:
describe "Revision in root folder" do
before do
with_user(#guest) do
visit revisions_path
end
end
it { should have_selector('div.alert.alert-error', text: auth_error_text) }
end
...
describe "Revision in root folder" do
before do
with_user(#user1) do
visit revisions_path
end
end
it { should have_selector('div.alert.alert-error', text: auth_error_text) }
end
The only parameter is the user calling with_user. Can I somehow use only one describe block, and iterate through an array of users, to keep my test DRY. It is important, that #guest and #user1 are created in a before(:all) block, so they are not available at the parsing of the spec.
Any help is appreciated.
describe "Revision in root folder" do
users = [#guest, #user1]
users.each do |user|
before do
with_user(user) do
visit revisions_path
end
end
it { should have_selector('div.alert.alert-error', text: auth_error_text) }
end
end
Not that much DRYer, but do you mind nesting your specs? This way you'll be able to account for any different expected behaviour between users and guests.
describe "Revision in root folder" do
context "as a guest" do
before do
with_user(#guest) do
visit revisions_path
end
end
it { should have_selector('div.alert.alert-error', text: auth_error_text) }
end
context "as a user" do
before do
with_user(#user1) do
visit revisions_path
end
end
it { should have_selector('div.alert.alert-error', text: auth_error_text) }
end
end
If you end up with many more duplicate it statements, you could probably refactor them up into a shared example.
I had the same problem and I resolve it in following way:
[:user_type_1, :user_type_2].each do |user_type|
let(:user) { create(user_type) }
before do
with_user(user) do
visit revisions_path
end
end
it { should have_selector('div.alert.alert-error', text: auth_error_text) }
end
Modern version of Rspec allows to duplicate examples without monkey-patching. Please have a look to this gist https://gist.github.com/SamMolokanov/713efc170d4ac36c5d5a16024ce633ea
different users might be provided as a shared_context - user will be available in actual tests:
shared_context "Tested user" do
let(:user) { |example| example.metadata[:user] }
end
During clone, we can do
USERS.each { |user| example.duplicate_with(user: user) }
context "answer is correct" do
before(:each) do
#answer = stub_model(Answer, :correct => true).as_new_record
assigns[:answer] = #answer
render "answers/summarize"
end
it "should display flashcard context properly" do
response.should contain("Quiz")
end
it "should summarize results" do
response.should contain("is correct")
end
end
context "answer is incorrect" do
before(:each) do
#answer = stub_model(Answer, :correct => false).as_new_record
assigns[:answer] = #answer
render "answers/summarize"
end
it "should display flashcard context properly" do
response.should contain("Quiz")
end
it "should summarize results" do
response.should contain("is incorrect")
end
end
How do I avoid repeating the following block within both of the above contexts?
it "should display flashcard context properly" do
response.should contain("Quiz")
end
Your specs describe the behavior you expect from your code - this amount of repetition is ok.
If it gets out of hand, use different contexts to isolate different expectations. For example, you could factor those duplicate expectations into a new context that just tests the page no matter what the Answer is.
If you really want to wrap up some of your tests, you can do it like this:
def answer_tests
it "should display flashcard context properly" do
response.should contain "Quiz"
end
it "should do be awesome" do
response.should be_awesome
end
end
context "answer is correct" do
answer_tests
it "should summarize results" do
response.should contain "is correct"
end
end
context "answer is incorrect" do
answer_tests
it "should summarize results" do
response.should contain "is incorrect"
end
end
As you can see, this is really handy when you have more than one test you want to wrap up in a method.