RSpec refactoring - ruby-on-rails

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 :-)

Related

This code can be rails unit test with rspec?

I'm studying rails and rspec.
And I made rspec unit test (request test) on rails application.
But after searching on google, I'm wonder if my job is on right way.
Can my code be a "Unit test by function(not a method, web site's feature ex)create, show, delete..) of rails application" ?
this is my code with request test.
require 'rails_helper'
RSpec.describe 'Users', type: :request do
let!(:users) { create_list(:user, 10) }
let(:user_id) { users.first.id }
let(:user) { create(:user) }
def send_request_to_store_user(name, mailaddress)
post '/users', params: {
user: {
name: users.first.name,
mailaddress: users.first.mailaddress
}
}
end
def http_status_success_and_body_element_check(body_element)
expect(response).to have_http_status(:success)
expect(response.body).to include(body_element)
end
describe 'GET' do
context 'Get /users test' do
it 'test user list page' do
get '/users'
http_status_success_and_body_element_check('User List')
end
end
context 'Get /users/create test' do
it 'test user create page' do
get '/users/create'
http_status_success_and_body_element_check('create user')
end
end
context 'Get /users/:id/edit' do
it 'test user edit page' do
get "/users/#{user_id}"
http_status_success_and_body_element_check('edit user')
end
end
context 'Get /users/:id' do
it 'test user show page' do
get "/users/#{user_id}"
http_status_success_and_body_element_check('show user')
end
end
end
describe 'POST' do
context 'test store new user' do
it 'test create new user' do
send_request_to_store_user(user.name, user.mailaddress)
expect do
create(:user)
end.to change { User.count }.from(User.count).to(User.count + 1)
end
it 'test redirect after create' do
send_request_to_store_user(user.name, user.mailaddress)
expect(response).to have_http_status(302)
end
end
end
describe 'DELETE' do
it 'test delete user' do
expect do
delete "/users/#{user_id}"
end.to change { User.count }.from(User.count).to(User.count - 1)
expect(response).to have_http_status(302)
end
end
describe 'PUT' do
context 'user update' do
it 'test user information update' do
old_name = users.first.name
new_name = 'new_name'
expect do
put "/users/#{user_id}", params: {
user: {
name: new_name
}
}
end.to change { users.first.reload.name }.from(old_name).to(new_name)
expect(response).to have_http_status(:redirect)
end
end
end
end
this is my code with test on model
require 'rails_helper'
RSpec.describe User, type: :model do
it 'user must have name and mailaddress' do
user = create(:user)
expect(user).to be_valid
expect(user.name).not_to be_nil
expect(user.mailaddress).not_to be_nil
end
it 'mailaddress must include #' do
# user = FactoryBot.create(:user)
# If rails_helper.rb has config.include FactoryBot::Syntax::Methods,
# Can use shortcut. Don't have to FactoryBot.create
user = create(:user)
# Test pass if email match with regexp
expect(user.mailaddress).to match(/\A[\w+\-.]+#[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/)
end
end
I don't think these tests are valuable (meaningful).
Here's my reasoning:
What are these tests telling you? That the Rails router is working? That the controller is responding with the right action? Neither of these are your responsibility to test. Rails has that covered.
If you want to know "does the index page render?" and "can I CRUD a user?" then write system tests with Capybara that simulate the whole flow. That way you are testing the real-world interaction with your whole system.

In views tests, when to use before(:all)?

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.

Rails ActionController::UrlGenerationError

I have basic rspec tests for my Client model. And it broke after I assigned it to current_user. I have problems figuring out what's wrong. Looking for some help.
Problem exist with show, edit and delete actions
require 'spec_helper'
describe 'ClientPage' do
subject {page}
let(:user) {FactoryGirl.create(:user)}
let(:client) {user.clients.build(client_name: client_name, client_secondname: client_secondname,
budget: budget, project: project)}
let(:client_name) {'Client'}
let(:client_secondname) {'First'}
let(:budget) {3000}
let(:project) {'Project'}
before {sign_in(user)}
#==============================New page===========>>
describe 'new client page' do
before {visit new_client_path}
it {should have_title('New client')}
let(:submit) {"Create"}
describe 'create' do
context 'invalid creation' do
it 'should not create client' do
expect{click_button submit}.not_to change(Client, :count)
end
end
context 'valid client creation' do
before do
fill_in 'Client name', with: client_name
fill_in 'Client secondname', with: client_secondname
fill_in 'Budget', with: budget
fill_in 'Project', with: project
end
it 'should create client' do
expect{click_button submit}.to change(Client, :count).by(1)
end
end
end
end
#==============================Show page===========>>
describe 'show' do
before {visit client_path(client)}
it {should have_title("#{client.combine_names} profile")}
end
#==============================Edit page===========>>
describe 'edit' do
let(:reload_cn) {client_name*2}
let(:reload_csn) {client_secondname*2}
let(:reload_bgt) {client.budget*2}
let(:reload_prg) {client.project*2}
before {visit edit_client_path(client)}
it {should have_title('Edit client panel')}
context 'successfull edit' do
before do
fill_in 'Client name', with: reload_cn
fill_in 'Client secondname', with: reload_csn
fill_in 'Budget', with: reload_bgt
fill_in 'Project', with: reload_prg
click_button('Save')
end
it {should have_content(reload_cn)}
it {should have_content(reload_csn)}
it {should have_content(reload_bgt)}
it {should have_content(reload_prg)}
end
end
#==============================Delete action===========>>
describe 'delete' do
before do
#client = user.clients.build(client_name: client_name, client_secondname: client_secondname, budget: budget, project: project)
visit root_path
end
it 'should delete client' do
expect{#client.delete}.to change(Client, :count).by(-1)
end
end
end
Solved. I've missed one thing - build do not save to database so example won't have an id.
create or FactoryGirl hepls.

Rspec. Delete action doesn't work with let()

I have Rspec testing controller actions via visiting pages.
And when I'm declaring new object through let() for create and edit actions - it's all fine. But for delete it doesn't work and I have to declare instance variable to make it pass.
My goal is to get rid of instance variable declaration in delete action.
It stops me from finishing re-factoring and I want to know why this happen?
Maybe it's somehow related to Capybara scope.
Failed tests looks like:
Failure/Error: it {should have_content('delete')}
expected #has_content?("delete") to return true, got false
and
Failure/Error: expect{click_link('delete')}.to change(Crew, :count).by(-1)
Capybara::ElementNotFound:
Unable to find link "delete"
My whole test
require 'spec_helper'
describe 'CrewPage' do
subject {page}
let(:user) {FactoryGirl.create(:user)}
let(:crew) {Crew.create(name: name, secondname: secondname, rate: rate)}
let(:name) {'Name'}
let(:secondname) {'First'}
let(:rate) {1000}
before {sign_in(user)}
#==============================New page===========>>
describe 'new crew member page' do
before {visit new_crew_path}
it {should have_title('New member')}
let(:submit) {"Create"}
context 'invalid creation' do
it 'should not create crew member' do
expect{click_button submit}.not_to change(Crew, :count)
end
end
context 'valid crew member creation' do
before do
fill_in 'Name', with: name
fill_in 'Secondname', with: secondname
fill_in 'Rate', with: rate
end
it 'should create crew member' do
expect{click_button submit}.to change(Crew, :count).by(1)
end
end
end
#==============================Show page===========>>
describe 'show page' do
before {visit crew_path(crew)}
it {should have_title("#{crew.combine_names} profile")}
end
#==============================Edit page===========>>
describe 'edit page' do
let(:reload_n) {name*2}
let(:reload_sn) {secondname*2}
let(:reload_r) {rate*2}
before {visit edit_crew_path(crew)}
it {should have_title('Edit panel')}
context 'successfull edit' do
before do
fill_in 'Name', with: reload_n
fill_in 'Secondname', with: reload_sn
fill_in 'Rate', with: reload_r
click_button('Save')
end
it {should have_content(reload_n)}
it {should have_content(reload_sn)}
it {should have_content(reload_r)}
end
end
#==============================Delete action===========>>
describe 'delete action from index page' do
before do
#crew = Crew.create(name: name, secondname: secondname, rate: rate)
visit root_path
end
it {should have_content('delete')}
it 'should delete crew member' do
expect{click_link('delete')}.to change(Crew, :count).by(-1)
end
end
end
let block is not executed unless it is called in the test itself. Because you are not using this variable prior to visit_page, crew model is not created and hence it is not displayed on the page capybara is playing with. If you want it to execute before each test, you have to call let! instead:
let!(:crew) { Crew.create(name: name, secondname: secondname, rate: rate)}

Rspec: tests in after block

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.

Resources