Let There Be Tests
Jo Cranford
The ratio of time spent reading
(code) versus writing is well over
10 to 1 … (therefore) making it
easy to read makes it easier to
write.
“
Robert C Martin
RSpec.describe Survey do
it 'closed? returns true when the status is closed' do
survey = Survey.new(status: :closed)
expect(survey.closed?).to be true
end
end
Describe Blocks
• Convention is one spec file for each Ruby file 

• describe block becomes ExampleGroup subclass with
metadata

• Pass RSpec a class or a description

• described_class is equal to the class definition 

• Each test -> instance of ExampleGroup subclass
RSpec.describe Survey do
describe '#close?' do
it 'is true when the status is closed' do
survey = Survey.new(status: :closed)
expect(survey.closed?).to be true
end
it 'is false when the status is closed' do
survey = Survey.new(status: :open)
expect(survey.closed?).to be false
end
end
end
RSpec.describe Survey do
describe '#close?' do
context 'when status is closed' do
it 'is true' do
survey = Survey.new(status: :closed)
expect(survey.closed?).to be true
end
end
context 'when status is open' do
it 'is false' do
survey = Survey.new(status: :open)
expect(survey.closed?).to be false
end
end
end
end
RSpec.describe SurveyHelper do
describe '#survey_color_indicator' do
context 'when the survey is archived' do
context 'and status is active' do
it 'is archived' do
survey = Survey.new(archived: true, status: :active)
expect(survey_color_indicator(survey)).to eq('archived')
end
end
context 'and status is closed' do
it 'is archived' do
survey = Survey.new(archived: true, status: :closed)
expect(survey_color_indicator(survey)).to eq('archived')
end
end
end
context 'when the survey is not archived' do
context 'and status is active' do
it 'is active' do
survey = Survey.new(archived: false, status: :active)
expect(survey_color_indicator(survey)).to eq('active')
end
end
context 'and status is closed' do
it 'is closed' do
survey = Survey.new(archived: false, status: :closed)
expect(survey_color_indicator(survey)).to eq('closed')
end
end
end
end
end
RSpec.describe SurveyHelper do
describe '#survey_color_indicator' do
it 'is archived when the survey is archived and status is active' do
survey = Survey.new
survey.archived = true
survey.status = :active
expect(survey_color_indicator(survey)).to eq('archived')
end
it 'is archived when the survey is archived and status is closed' do
survey = Survey.new
survey.archived = true
survey.status = :closed
expect(survey_color_indicator(survey)).to eq('archived')
end
it 'is active when the survey is not archived and status is active' do
survey = Survey.new
survey.archived = false
survey.status = :active
expect(survey_color_indicator(survey)).to eq('active')
end
it 'is closed when the survey is not archived and status is closed' do
survey = Survey.new
survey.archived = false
survey.status = :closed
expect(survey_color_indicator(survey)).to eq('closed')
end
end
end
Before and After
RSpec.describe SurveyAdminController do
describe '#index' do
before do
Survey.create!(name: 'My Survey')
get :index
end
after do
Survey.destroy_all
end
it 'retrieves the surveys' do
expect(assigns(:surveys).size).to eq 1
end
end
end
Around
RSpec.describe SurveyAdminController do
describe '#index' do
around do |example|
Survey.create!(name: 'My Survey')
get :index
example.run
Survey.destroy_all
end
it 'retrieves the surveys' do
expect(assigns[:surveys].size).to eq 1
end
end
end
Conditional Before
RSpec.configure do |config|
config.before(:example, :authorized => true) do
log_in_as :authorized_user
end
end
describe Something, :authorized => true do
# The before hook will run in before each example in this group.
end
describe SomethingElse do
it "does something", :authorized => true do
# The before hook will run before this example.
end
it "does something else" do
# The hook will not run before this example.
end
end
RSpec.describe SurveyHelper do
describe '#survey_color_indicator' do
before do
@survey = Survey.new
end
context 'when the survey is archived' do
before do
@survey.archived = true
end
context 'and status is active' do
before do
@survey.status = :active
end
it 'is archived' do
expect(survey_color_indicator(@survey)).to eq('archived')
end
end
context 'and status is closed' do
before do
@survey.status = :closed
end
it 'is archived' do
expect(survey_color_indicator(@survey)).to eq('archived')
end
end
end
end
end
Instance Variables Antipattern
• Evaluated for every single test

-> slows tests down

• Spring into existence the first time that they are evaluated

-> subtle bugs can creep in

• Can’t be overridden in nested blocks

-> duplicated code
RSpec.describe SurveyHelper do
describe '#survey_color_indicator' do
before do
@survey = Survey.new
end
context 'when the survey is archived' do
before do
@survey.archived = true
end
context 'and status is active' do
before do
@survey.status = :active
end
it 'is archived' do
expect(survey_color_indicator(@survey)).to eq('archived')
end
end
context 'and status is closed' do
before do
@survey.status = :closed
end
it 'is archived' do
expect(survey_color_indicator(@survey)).to eq('archived')
end
end
end
end
end
Let vs @
RSpec.describe SurveyHelper do
describe '#survey_color_indicator' do
let(:survey) { Survey.new(archived: is_archived, status: status) }
context 'when the survey is archived' do
let(:archived) { true }
context 'and status is active' do
let(:status) { :active }
it 'is archived' do
expect(survey_color_indicator(survey)).to eq('archived')
end
end
context 'and status is closed' do
let(:status) { :closed }
it 'is archived' do
expect(survey_color_indicator(survey)).to eq('archived')
end
end
end
end
end
Nested lets
RSpec.describe SurveyAdminController do
describe '#update' do
let(:survey) { Survey.create!(name: 'My Survey') }
let(:params) { { survey_id: survey.id } }
before do
post :update, params
end
context 'when no updates are specified' do
it 'does not change the survey' do
expect(survey.name).to eq 'My Survey'
end
end
context 'when the survey name is changed' do
let(:params) { { survey_id: survey.id, name: 'New Name' } }
it 'updates the name' do
expect(survey.name).to eq 'New Name'
end
end
end
end
let!
describe SurveyAdminController do
describe '#index' do
let!(:survey) { Survey.create!(name: 'My Survey') }
before do
get :index
end
after do
Survey.destroy_all
end
it 'retrieves the surveys' do
expect(assigns[:surveys]).to include(survey)
end
end
end
Execution Order
RSpec.describe Something do
let!(:some_variable) do
# First
end
before do
# Second
end
describe 'Nested block' do
let!(:nested_variable) do
# Third
end
before do
# Fourth
end
let!(:another_one) do
# Fifth
end
end
end
Subject
• Originally introduced to allow one line syntax

• Recommended by Better Specs to DRY tests

• Sometimes difficult to figure out what the subject of
a test actually is

• Proceed with caution …
Implicit and Explicit Subject
# Explicit
describe Person do
subject { Person.new(:birthdate => 19.years.ago) }
it "should be eligible to vote" do
subject.should be_eligible_to_vote
# ^ ^ explicit reference to subject not recommended
end
end
# Implicit subject => { Person.new }.
describe Person do
it "should be eligible to vote" do
subject.should be_eligible_to_vote
# ^ ^ explicit reference to subject not recommended
end
end
One Line Syntax
describe SurveyResults::Employee do
let(:id) { 123 }
let(:email) { "man@moon.com" }
subject { described_class.new(id: id, email: email) }
it { is_expected.to have_attributes(id: 123) }
it { is_expected.to have_attributes(email: "man@moon.com") }
it { is_expected.to have_attributes(submission: nil) }
end
Summary
• We all spend a lot of time reading and understanding code 

-> Writing clearer code and tests helps us go faster

• Nested describe and context blocks separate tests into logical blocks

• Use before to group common set up code

• let over @instance_variables to keep tests DRY, clear and performant

• let! behaves like a before, be aware of the order of execution

• Explicit subject and one line syntax make tests more succinct 

• Avoid compromising readability
References
• Better Specs 

http://betterspecs.org/

• RSpec docs

http://www.relishapp.com/rspec/rspec-core/v/3-0/docs

• RSpec source code

https://github.com/rspec/rspec-core

• RSpec: It’s not actually magic (RailsConf US 2015)

https://www.youtube.com/watch?v=Libc0-0TRg4
P.S We’re hiring!
jo@cultureamp.com
Thanks!

R spec let there be tests

  • 1.
    Let There BeTests Jo Cranford
  • 2.
    The ratio oftime spent reading (code) versus writing is well over 10 to 1 … (therefore) making it easy to read makes it easier to write. “ Robert C Martin
  • 3.
    RSpec.describe Survey do it'closed? returns true when the status is closed' do survey = Survey.new(status: :closed) expect(survey.closed?).to be true end end
  • 4.
    Describe Blocks • Conventionis one spec file for each Ruby file • describe block becomes ExampleGroup subclass with metadata • Pass RSpec a class or a description • described_class is equal to the class definition • Each test -> instance of ExampleGroup subclass
  • 5.
    RSpec.describe Survey do describe'#close?' do it 'is true when the status is closed' do survey = Survey.new(status: :closed) expect(survey.closed?).to be true end it 'is false when the status is closed' do survey = Survey.new(status: :open) expect(survey.closed?).to be false end end end
  • 6.
    RSpec.describe Survey do describe'#close?' do context 'when status is closed' do it 'is true' do survey = Survey.new(status: :closed) expect(survey.closed?).to be true end end context 'when status is open' do it 'is false' do survey = Survey.new(status: :open) expect(survey.closed?).to be false end end end end
  • 7.
    RSpec.describe SurveyHelper do describe'#survey_color_indicator' do context 'when the survey is archived' do context 'and status is active' do it 'is archived' do survey = Survey.new(archived: true, status: :active) expect(survey_color_indicator(survey)).to eq('archived') end end context 'and status is closed' do it 'is archived' do survey = Survey.new(archived: true, status: :closed) expect(survey_color_indicator(survey)).to eq('archived') end end end context 'when the survey is not archived' do context 'and status is active' do it 'is active' do survey = Survey.new(archived: false, status: :active) expect(survey_color_indicator(survey)).to eq('active') end end context 'and status is closed' do it 'is closed' do survey = Survey.new(archived: false, status: :closed) expect(survey_color_indicator(survey)).to eq('closed') end end end end end RSpec.describe SurveyHelper do describe '#survey_color_indicator' do it 'is archived when the survey is archived and status is active' do survey = Survey.new survey.archived = true survey.status = :active expect(survey_color_indicator(survey)).to eq('archived') end it 'is archived when the survey is archived and status is closed' do survey = Survey.new survey.archived = true survey.status = :closed expect(survey_color_indicator(survey)).to eq('archived') end it 'is active when the survey is not archived and status is active' do survey = Survey.new survey.archived = false survey.status = :active expect(survey_color_indicator(survey)).to eq('active') end it 'is closed when the survey is not archived and status is closed' do survey = Survey.new survey.archived = false survey.status = :closed expect(survey_color_indicator(survey)).to eq('closed') end end end
  • 8.
    Before and After RSpec.describeSurveyAdminController do describe '#index' do before do Survey.create!(name: 'My Survey') get :index end after do Survey.destroy_all end it 'retrieves the surveys' do expect(assigns(:surveys).size).to eq 1 end end end
  • 9.
    Around RSpec.describe SurveyAdminController do describe'#index' do around do |example| Survey.create!(name: 'My Survey') get :index example.run Survey.destroy_all end it 'retrieves the surveys' do expect(assigns[:surveys].size).to eq 1 end end end
  • 10.
    Conditional Before RSpec.configure do|config| config.before(:example, :authorized => true) do log_in_as :authorized_user end end describe Something, :authorized => true do # The before hook will run in before each example in this group. end describe SomethingElse do it "does something", :authorized => true do # The before hook will run before this example. end it "does something else" do # The hook will not run before this example. end end
  • 11.
    RSpec.describe SurveyHelper do describe'#survey_color_indicator' do before do @survey = Survey.new end context 'when the survey is archived' do before do @survey.archived = true end context 'and status is active' do before do @survey.status = :active end it 'is archived' do expect(survey_color_indicator(@survey)).to eq('archived') end end context 'and status is closed' do before do @survey.status = :closed end it 'is archived' do expect(survey_color_indicator(@survey)).to eq('archived') end end end end end
  • 12.
    Instance Variables Antipattern •Evaluated for every single test
 -> slows tests down • Spring into existence the first time that they are evaluated
 -> subtle bugs can creep in • Can’t be overridden in nested blocks
 -> duplicated code
  • 13.
    RSpec.describe SurveyHelper do describe'#survey_color_indicator' do before do @survey = Survey.new end context 'when the survey is archived' do before do @survey.archived = true end context 'and status is active' do before do @survey.status = :active end it 'is archived' do expect(survey_color_indicator(@survey)).to eq('archived') end end context 'and status is closed' do before do @survey.status = :closed end it 'is archived' do expect(survey_color_indicator(@survey)).to eq('archived') end end end end end
  • 14.
    Let vs @ RSpec.describeSurveyHelper do describe '#survey_color_indicator' do let(:survey) { Survey.new(archived: is_archived, status: status) } context 'when the survey is archived' do let(:archived) { true } context 'and status is active' do let(:status) { :active } it 'is archived' do expect(survey_color_indicator(survey)).to eq('archived') end end context 'and status is closed' do let(:status) { :closed } it 'is archived' do expect(survey_color_indicator(survey)).to eq('archived') end end end end end
  • 15.
    Nested lets RSpec.describe SurveyAdminControllerdo describe '#update' do let(:survey) { Survey.create!(name: 'My Survey') } let(:params) { { survey_id: survey.id } } before do post :update, params end context 'when no updates are specified' do it 'does not change the survey' do expect(survey.name).to eq 'My Survey' end end context 'when the survey name is changed' do let(:params) { { survey_id: survey.id, name: 'New Name' } } it 'updates the name' do expect(survey.name).to eq 'New Name' end end end end
  • 16.
    let! describe SurveyAdminController do describe'#index' do let!(:survey) { Survey.create!(name: 'My Survey') } before do get :index end after do Survey.destroy_all end it 'retrieves the surveys' do expect(assigns[:surveys]).to include(survey) end end end
  • 17.
    Execution Order RSpec.describe Somethingdo let!(:some_variable) do # First end before do # Second end describe 'Nested block' do let!(:nested_variable) do # Third end before do # Fourth end let!(:another_one) do # Fifth end end end
  • 18.
    Subject • Originally introducedto allow one line syntax • Recommended by Better Specs to DRY tests • Sometimes difficult to figure out what the subject of a test actually is • Proceed with caution …
  • 19.
    Implicit and ExplicitSubject # Explicit describe Person do subject { Person.new(:birthdate => 19.years.ago) } it "should be eligible to vote" do subject.should be_eligible_to_vote # ^ ^ explicit reference to subject not recommended end end # Implicit subject => { Person.new }. describe Person do it "should be eligible to vote" do subject.should be_eligible_to_vote # ^ ^ explicit reference to subject not recommended end end
  • 20.
    One Line Syntax describeSurveyResults::Employee do let(:id) { 123 } let(:email) { "man@moon.com" } subject { described_class.new(id: id, email: email) } it { is_expected.to have_attributes(id: 123) } it { is_expected.to have_attributes(email: "man@moon.com") } it { is_expected.to have_attributes(submission: nil) } end
  • 21.
    Summary • We allspend a lot of time reading and understanding code 
 -> Writing clearer code and tests helps us go faster • Nested describe and context blocks separate tests into logical blocks • Use before to group common set up code • let over @instance_variables to keep tests DRY, clear and performant • let! behaves like a before, be aware of the order of execution • Explicit subject and one line syntax make tests more succinct • Avoid compromising readability
  • 22.
    References • Better Specs
 http://betterspecs.org/ • RSpec docs
 http://www.relishapp.com/rspec/rspec-core/v/3-0/docs • RSpec source code
 https://github.com/rspec/rspec-core • RSpec: It’s not actually magic (RailsConf US 2015)
 https://www.youtube.com/watch?v=Libc0-0TRg4
  • 23.