Skip to content

Instantly share code, notes, and snippets.

@esmerino
Created April 17, 2014 21:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save esmerino/11012237 to your computer and use it in GitHub Desktop.
Save esmerino/11012237 to your computer and use it in GitHub Desktop.
RSPEC
gem 'rspec-rails', group: [:development, :test]
gem 'capybara', group: [:test]
gem 'factory_girl_rails', group: [:test]
gem 'faker', group: [:test]
#Expectations for RSpec
https://github.com/rspec/rspec-expectations
#You can chain the two rake tasks together on your command line with
rake db:migrate db:test:clone
#Introdution
"
• Tests should be reliable.
• Tests should be easy to write.
• Tests should be easy to understand.
"
#Model specs
"
• First we’ll create a model spec for an existing model–in our case, the actual Contact model.
• Then, we’ll write passing tests for a model’s validations, class, and instance methods, and
organize our spec in the process.
"
##Anatomy of a model spec
"
• The model’s create method, when passed valid attributes, should be valid.
• Data that fail validations should not be valid.
• Class and instance methods perform as expected.
"
##Testing validations
describe Contact do
it "is invalid without a firstname" do
expect(Contact.new(firstname: nil)).to have(1).errors_on(:firstname)
end
it "is invalid with a duplicate email address" do
Contact.create(
firstname: 'Joe', lastname: 'Tester',
email: 'tester@example.com')
contact = Contact.new(
firstname: 'Jane', lastname: 'Tester',
email: 'tester@example.com')
expect(contact).to have(1).errors_on(:email)
end
end
app/models/phone.rb
validates :phone, uniqueness: { scope: :contact_id }
describe Phone do
it "does not allow duplicate phone numbers per contact" do
contact = Contact.create(firstname: 'Joe', lastname: 'Tester',
email: 'joetester@example.com')
contact.phones.create(phone_type: 'home',
phone: '785-555-1234')
mobile_phone = contact.phones.build(phone_type: 'mobile',
phone: '785-555-1234')
expect(mobile_phone).to have(1).errors_on(:phone)
end
it "allows two contacts to share a phone number" do
contact = Contact.create(firstname: 'Joe', lastname: 'Tester',
email: 'joetester@example.com')
contact.phones.create(phone_type: 'home',
phone: '785-555-1234')
other_contact = Contact.new
other_phone = other_contact.phones.build(phone_type:
'home', phone: '785-555-1234')
expect(other_phone).to be_valid
end
end
##Testing instance methods
app/models/contact.rb
def name
[firstname, lastname].join(' ')
end
describe Contact do
it "returns a contact's full name as a string" do
contact = Contact.new(firstname: 'John', lastname: 'Doe',
email: 'johndoe@example.com')
expect(contact.name).to eq 'John Doe'
end
end
## Testing class methods and scopes
app/models/contact.rb
def self.by_letter(letter)
where("lastname LIKE ?", "#{letter}%").order(:lastname)
end
describe Contact do
it "returns a sorted array of results that match" do
smith = Contact.create(firstname: 'John', lastname: 'Smith',
email: 'jsmith@example.com')
jones = Contact.create(firstname: 'Tim', lastname: 'Jones',
email: 'tjones@example.com')
johnson = Contact.create(firstname: 'John', lastname: 'Johnson',
email: 'jjohnson@example.com')
expect(Contact.by_letter("J")).to eq [johnson, jones]
end
end
#DRYer specs with describe, context, before and after
spec/models/contact_spec.rb
require 'spec_helper'
describe Contact do
describe "filter last name by letter" do
end
end
spec/models/contact_spec.rb
require 'spec_helper'
describe Contact do
describe "filter last name by letter" do
context "matching letters" do
end
context "non-matching letters" do
end
end
end
spec/models/contact_spec.rb
require 'spec_helper'
describe Contact do
describe "filter last name by letter" do
before :each do
@smith = Contact.create(firstname: 'John', lastname: 'Smith',
email: 'jsmith@example.com')
@jones = Contact.create(firstname: 'Tim', lastname: 'Jones',
email: 'tjones@example.com')
@johnson = Contact.create(firstname: 'John', lastname: 'Johnson',
email: 'jjohnson@example.com')
end
context "matching letters" do
end
context "non-matching letters" do
end
end
end
##### Code
spec/models/contact_spec.rb
require 'spec_helper'
describe Contact do
it "is valid with a firstname, lastname and email" do
contact = Contact.new(
firstname: 'Aaron',
lastname: 'Sumner',
email: 'tester@example.com')
expect(contact).to be_valid
end
it "is invalid without a firstname" do
expect(Contact.new(firstname: nil)).to have(1).errors_on(:firstname)
end
it "is invalid without a lastname" do
expect(Contact.new(lastname: nil)).to have(1).errors_on(:lastname)
end
it "is invalid without an email address" do
expect(Contact.new(email: nil)).to have(1).errors_on(:email)
end
it "is invalid with a duplicate email address" do
Contact.create(
firstname: 'Joe', lastname: 'Tester',
email: 'tester@example.com')
contact = Contact.new(
firstname: 'Jane', lastname: 'Tester',
email: 'tester@example.com')
expect(contact).to have(1).errors_on(:email)
end
it "returns a contact's full name as a string" do
contact = Contact.new(firstname: 'John', lastname: 'Doe',
email: 'johndoe@example.com')
expect(contact.name).to eq 'John Doe'
end
describe "filter last name by letter" do
before :each do
@smith = Contact.create(firstname: 'John', lastname: 'Smith',
email: 'jsmith@example.com')
@jones = Contact.create(firstname: 'Tim', lastname: 'Jones',
email: 'tjones@example.com')
@johnson = Contact.create(firstname: 'John', lastname: 'Johnson',
email:'jjohnson@example.com')
end
context "matching letters" do
it "returns a sorted array of results that match" do
expect(Contact.by_letter("J")).to eq [@johnson, @jones]
end
end
context "non-matching letters" do
it "returns a sorted array of results that match" do
expect(Contact.by_letter("J")).to_not include @smith
end
end
end
end
#####
### Generating test data with factories
"
Enter factories: Simple, flexible, building blocks for test data. If I had to point to a single
component that helped me see the light toward testing more than anything else, it would
be Factory Girl18, an easy-to-use and easy-to-rely-on gem for creating test data without the
brittleness of fixtures.
"
spec/factories/contacts.rb
FactoryGirl.define do
factory :contact do
firstname "John"
lastname "Doe"
sequence(:email) { |n| "johndoe#{n}@example.com"}
end
end
spec/models/contact_spec.rb
it "is invalid without a firstname" do
contact = FactoryGirl.build(:contact, firstname: nil)
expect(contact).to have(1).errors_on(:firstname)
end
it "is invalid without a lastname" do
contact = FactoryGirl.build(:contact, lastname: nil)
expect(contact).to have(1).errors_on(:lastname)
end
it "is invalid without an email address" do
contact = FactoryGirl.build(:contact, email: nil)
expect(contact).to have(1).errors_on(:email)
end
it "returns a contact's full name as a string" do
contact = FactoryGirl.build(:contact,
firstname: "Jane", lastname: "Doe")
expect(contact.name).to eq "Jane Doe"
end
it "is invalid with a duplicate email address" do
FactoryGirl.create(:contact, email: "aaron@example.com")
contact = FactoryGirl.build(:contact, email: "aaron@example.com")
expect(contact).to have(1).errors_on(:email)
end
#Simplifying our syntax
spec/spec_helper.rb
RSpec.configure do |config|
config.include FactoryGirl::Syntax::Methods
end
require 'spec_helper'
describe Contact do
it "has a valid factory" do
expect(build(:contact)).to be_valid
end
it "is invalid without a firstname" do
expect(build(:contact, firstname: nil)).to \
have(1).errors_on(:firstname)
end
it "is invalid without a lastname" do
expect(build(:contact, lastname: nil)).to \
have(1).errors_on(:lastname)
end
end
# Associations and inheritance in factories
spec/factories/phones.rb
FactoryGirl.define do
factory :phone do
association :contact
phone { '123-555-1234' }
phone_type 'home'
end
end
spec/models/phone_spec.rb
it "allows two contacts to share a phone number" do
create(:phone,
phone_type: 'home',
phone: "785-555-1234")
expect(build(:phone,
phone_type: 'home',
phone: "785-555-1234")).to be_valid
end
spec/factories/phones.rb
FactoryGirl.define do
factory :phone do
association :contact
phone { '123-555-1234' }
factory :home_phone do
phone_type 'home'
end
factory :work_phone do
phone_type 'work'
end
factory :mobile_phone do
phone_type 'mobile'
end
end
end
spec/models/phone_spec.rb
require 'spec_helper'
describe Phone do
it "does not allow duplicate phone numbers per contact" do
contact = create(:contact)
create(:home_phone,
contact: contact,
phone: '785-555-1234')
mobile_phone = build(:mobile_phone,
contact: contact,
phone: '785-555-1234')
expect(mobile_phone).to have(1).errors_on(:phone)
end
it "allows two contacts to share a phone number" do
create(:home_phone,
phone: "785-555-1234")
expect(build(:home_phone, phone: "785-555-1234")).to be_valid
end
end
#Generating more realistic fake data
spec/factories/contacts.rb
require 'faker'
FactoryGirl.define do
factory :contact do
firstname { Faker::Name.first_name }
lastname { Faker::Name.last_name }
email { Faker::Internet.email }
end
end
#Advanced associations
spec/factories/contacts.rb
require 'faker'
FactoryGirl.define do
factory :contact do
firstname { Faker::Name.first_name }
lastname { Faker::Name.last_name }
email { Faker::Internet.email }
after(:build) do |contact|
[:home_phone, :work_phone, :mobile_phone].each do |phone|
contact.phones << FactoryGirl.build(:phone,
phone_type: phone, contact: contact)
end
end
end
end
spec/models/contact_spec.rb
it "has three phone numbers" do
expect(create(:contact).phones.count).to eq 3
end
## Basic controller specs
"
.First, we’ll discuss why you should test controllers at all.
.We’ll follow that discussion with the very basics (or, controller specs are just unit specs).
.Next we’ll begin organizing controller specs in an outline-like format.
.We’ll then use factories to set up data for specs.
.Then we’ll test the seven CRUD methods included in most controllers, along with a non-CRUD example.
.Next, we’ll look at testing nested routes.
.We’ll wrap up with testing a controller method with non-HTML output, such as a file export.
"
# Setting up test data
spec/factories/contacts.rb
require 'faker'
FactoryGirl.define do
factory :contact do
firstname { Faker::Name.first_name }
lastname { Faker::Name.last_name }
email { Faker::Internet.email }
after(:build) do |contact|
[:home_phone, :work_phone, :mobile_phone].each do |phone|
contact.phones << FactoryGirl.build(:phone,
phone_type: phone, contact: contact)
end
end
factory :invalid_contact do
firstname nil
end
end
end
# Testing GET requests
spec/controllers/contacts_controller_spec.rb
describe 'GET #show' do
it "assigns the requested contact to @contact" do
contact = create(:contact)
get :show, id: contact
expect(assigns(:contact)).to eq contact
end
it "renders the :show template" do
contact = create(:contact)
get :show, id: contact
expect(response).to render_template :show
end
end
"
• The basic DSL for interacting with controller methods: Each HTTP verb has its own
method (in these cases, get), which expects the controller method name as a symbol (here,
:show), followed by any params (id: contact).
• Variables instantiated by the controller method can be evaluated using assigns(:variable_-
name).
• The finished product returned from the controller method can be evaluated through
response.
"
spec/controllers/contacts_controller_spec.rb
describe 'GET #index' do
context 'with params[:letter]' do
it "populates an array of contacts starting with the letter" do
smith = create(:contact, lastname: 'Smith')
jones = create(:contact, lastname: 'Jones')
get :index, letter: 'S'
expect(assigns(:contacts)).to match_array([smith])
end
it "renders the :index template" do
get :index, letter: 'S'
expect(response).to render_template :index
end
end
context 'without params[:letter]' do
it "populates an array of all contacts" do
smith = create(:contact, lastname: 'Smith')
jones = create(:contact, lastname: 'Jones')
get :index
expect(assigns(:contacts)).to match_array([smith, jones])
end
it "renders the :index template" do
get :index
expect(response).to render_template :index
end
end
end
spec/controllers/contacts_controller_spec.rb
describe 'GET #new' do
it "assigns a new Contact to @contact" do
get :new
expect(assigns(:contact)).to be_a_new(Contact)
end
it "renders the :new template" do
get :new
expect(response).to render_template :new
end
end
describe 'GET #edit' do
it "assigns the requested contact to @contact" do
contact = create(:contact)
get :edit, id: contact
expect(assigns(:contact)).to eq contact
end
it "renders the :edit template" do
contact = create(:contact)
get :edit, id: contact
expect(response).to render_template :edit
end
end
# Testing POST requests
it "does something upon post#create" do
post :create, contact: attributes_for(:contact)
end
spec/controllers/contacts_controller_spec.rb
describe "POST #create" do
before :each do
@phones = [
attributes_for(:phone),
attributes_for(:phone),
attributes_for(:phone)
]
end
context "with valid attributes" do
it "saves the new contact in the database" do
expect{
post :create, contact: attributes_for(:contact,
phones_attributes: @phones)
}.to change(Contact, :count).by(1)
end
it "redirects to contacts#show" do
post :create, contact: attributes_for(:contact,
phones_attributes: @phones)
expect(response).to redirect_to contact_path(assigns[:contact])
end
end
end
end
spec/controllers/contacts_controller_spec.rb
context "with invalid attributes" do
it "does not save the new contact in the database" do
expect{
post :create,
contact: attributes_for(:invalid_contact)
}.to_not change(Contact, :count)
end
it "re-renders the :new template" do
post :create,
contact: attributes_for(:invalid_contact)
expect(response).to render_template :new
end
end
end
# Testing PATCH requests
spec/controllers/contacts_controller_spec.rb
describe 'PATCH #update' do
before :each do
@contact = create(:contact,
firstname: 'Lawrence', lastname: 'Smith')
end
context "valid attributes" do
it "locates the requested @contact" do
patch :update, id: @contact, contact: attributes_for(:contact)
expect(assigns(:contact)).to eq(@contact)
end
it "changes @contact's attributes" do
patch :update, id: @contact,
contact: attributes_for(:contact,
firstname: "Larry", lastname: "Smith")
@contact.reload
expect(@contact.firstname).to eq("Larry")
expect(@contact.lastname).to eq("Smith")
end
it "redirects to the updated contact" do
patch :update, id: @contact, contact: attributes_for(:contact)
expect(response).to redirect_to @contact
end
end
end
spec/controllers/contacts_controller_spec.rb
describe 'PATCH #update' do
context "with invalid attributes" do
it "does not change the contact's attributes" do
patch :update, id: @contact,
contact: attributes_for(:contact,
firstname: "Larry", lastname: nil)
@contact.reload
expect(@contact.firstname).to_not eq("Larry")
expect(@contact.lastname).to eq("Smith")
end
it "re-renders the edit template" do
patch :update, id: @contact,
contact: attributes_for(:invalid_contact)
expect(response).to render_template :edit
end
end
end
# Testing DELETE requests
spec/controllers/contacts_controller_spec.rb
describe 'DELETE #destroy' do
before :each do
@contact = create(:contact)
end
it "deletes the contact" do
expect{ delete :destroy, id: @contact}.to change(Contact,:count).by(-1)
end
it "redirects to contacts#index" do
delete :destroy, id: @contact
expect(response).to redirect_to contacts_url
end
end
# Testing non-CRUD methods
describe "PATCH hide_contact" do
before :each do
@contact = create(:contact)
end
it "marks the contact as hidden" do
patch :hide_contact, id: @contact
expect(@contact.reload.hidden?).to be_true
end
it "redirects to contacts#index" do
patch :hide_contact, id: @contact
expect(response).to redirect_to contacts_url
end
end
# Testing nested routes
config/routes.rb
resources :contacts do
resources :phones
end
describe 'GET #show' do
it "renders the :show template for the phone" do
contact = create(:contact)
phone = create(:phone, contact: contact)
get :show, id: phone, contact_id: contact.id
expect(response).to render_template :show
end
end
# Testing non-HTML controller output
link_to 'Export', contacts_path(format: :csv)
def index
@contacts = Contact.all
respond_to do |format|
format.html
format.csv do
send_data Contact.to_csv(@contacts),
type: 'text/csv; charset=iso-8859-1; header=present',
disposition: 'attachment; filename=contacts.csv'
end
end
end
describe 'CSV output' do
it "returns a CSV file" do
get :index, format: :csv
expect(response.headers['Content-Type']).to have_content 'text/csv'
end
it 'returns content' do
create(:contact,
firstname: 'Aaron',
lastname: 'Sumner',
email: 'aaron@sample.com')
get :index, format: :csv
expect(response.body).to have_content 'Aaron Sumner,aaron@sample.com'
end
end
it "returns comma separated values" do
create(:contact,
firstname: 'Aaron',
lastname: 'Sumner',
email: 'aaron@sample.com')
expect(Contact.to_csv).to match /Aaron Sumner,aaron@sample.com/
end
it "returns JSON-formatted content" do
contact = create(:contact)
get :index, format: :json
expect(response.body).to have_content contact.to_json
end
# Advanced controller specs
spec/factories/users.rb
require 'faker'
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
password 'secret'
password_confirmation 'secret'
factory :admin do
admin true
end
end
end
spec/controllers/contacts_controller_spec.rb
describe "administrator access" do
before :each do
user = create(:admin)
session[:user_id] = user.id
end
describe 'GET #index' do
it "populates an array of contacts" do
get :index
expect(assigns(:contacts)).to match_array [@contact]
end
it "renders the :index template" do
get :index
expect(response).to render_template :index
end
end
describe 'GET #show' do
it "assigns the requested contact to @contact" do
get :show, id: @contact
expect(assigns(:contact)).to eq @contact
end
it "renders the :show template" do
get :show, id: @contact
expect(response).to render_template :show
end
end
end
# Testing the guest role
spec/controllers/contacts_controller_spec.rb
describe "guest access" do
describe 'GET #new' do
it "requires login" do
get :new
expect(response).to redirect_to login_url
end
end
describe 'GET #edit' do
it "requires login" do
contact = create(:contact)
get :edit, id: contact
expect(response).to redirect_to login_url
end
end
describe "POST #create" do
it "requires login" do
post :create, id: create(:contact),
contact: attributes_for(:contact)
expect(response).to redirect_to login_url
end
end
describe 'PUT #update' do
it "requires login" do
put :update, id: create(:contact),
contact: attributes_for(:contact)
expect(response).to redirect_to login_url
end
end
describe 'DELETE #destroy' do
it "requires login" do
delete :destroy, id: create(:contact)
expect(response).to redirect_to login_url
end
end
end
# Testing a given role’s authorization
spec/controllers/users_controller_spec.rb
describe 'user access' do
before :each do
@user = create(:user)
session[:user_id] = @user.id
end
describe 'GET #index' do
it "collects users into @users" do
user = create(:user)
get :index
expect(assigns(:users)).to match_array [@user,user]
end
it "renders the :index template" do
get :index
expect(response).to render_template :index
end
end
it "GET #new denies access" do
get :new
expect(response).to redirect_to root_url
end
it "POST#create denies access" do
post :create, user: attributes_for(:user)
expect(response).to redirect_to root_url
end
end
# Feature specs
gem "database_cleaner", group: :test
gem "launchy", group: :test
mkdir spec/features/
# A basic feature spec
spec/features/users_spec.rb
require 'spec_helper'
feature 'User management' do
scenario "adds a new user" do
admin = create(:admin)
visit root_path
click_link 'Log In'
fill_in 'Email', with: admin.email
fill_in 'Password', with: admin.password
click_button 'Log In'
visit root_path
expect{
click_link 'Users'
click_link 'New User'
fill_in 'Email', with: 'newuser@example.com'
find('#password').fill_in 'Password', with: 'secret123'
find('#password_confirmation').fill_in 'Password confirmation',with: 'secret123'
click_button 'Create User'
}.to change(User, :count).by(1)
expect(current_path).to eq users_path
expect(page).to have_content 'New user created'
within 'h1' do
expect(page).to have_content 'Users'
end
expect(page).to have_content 'newuser@example.com'
end
end
# Adding feature specs
require 'spec_helper'
feature 'my feature' do
background do
"add setup details"
end
scenario 'my first test' do
"write the example!"
end
end
spec/features/users_spec.rb
require 'spec_helper'
feature 'User management' do
scenario "adds a new user" do
admin = create(:admin)
sign_in admin
visit root_path
expect{
click_link 'Users'
click_link 'New User'
fill_in 'Email', with: 'newuser@example.com'
find('#password').fill_in 'Password', with: 'secret123'
find('#password_confirmation').fill_in 'Password confirmation',
with: 'secret123'
click_button 'Create User'
}.to change(User, :count).by(1)
save_and_open_page
end
end
# Including JavaScript interactions
spec/features/about_us_spec.rb
require 'spec_helper'
feature "About BigCo modal" do
scenario "toggles display of the modal about display" do
visit root_path
expect(page).to_not have_content 'About BigCo'
expect(page).to_not \
have_content 'BigCo produces the finest widgets in all the land'
click_link 'About Us'
expect(page).to have_content 'About BigCo'
expect(page).to \
have_content 'BigCo produces the finest widgets in all the land'
within '#about_us' do
click_button 'Close'
end
expect(page).to_not have_content 'About BigCo'
expect(page).to_not \
have_content 'BigCo produces the finest widgets in all the land'
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment