RSpec 3: The new, the old, the good
-
Upload
mglrnm -
Category
Engineering
-
view
426 -
download
0
description
Transcript of RSpec 3: The new, the old, the good
RSpec 3
Contents
1. Changes from v2.14 to v3.y.z2. Better Specs reviewed
Changes fromv2.14 to v3.y.z
Expect to not‘should’ anymore
Changes
● New syntax does not use “monkey patching”● Examples are based on “expectations” over objects
</>
What is it about?# ‘should’ is “replaced” by ‘expect’
#
it ‘stores 5 in its attribute’ do
expect(subject.attribute).to eq 5
end
</>
In one line# One-liner syntax changes accordingly
#
it { should eq 5 }
# Is written like...
#
it { is_expected.to eq 5 }
Migrating
● We can migrate our current specs to the new syntax using github.com/yujinakayama/transpec
● In 3.1.z, there’s still a mechanism to work with the old syntax
Don’t stub. Allow.
Changes
● New syntax to stub methods● Also a new syntax to set expectations on method
calls
</>
How to stub# Old
instance.stub(:method)
instance.stub(:method).with(<arguments>)
instance.stub(:method).with(<arguments>).and_return(<something>)
# New
allow(instance).to receive(:method)
allow(instance).to receive(:method).with(<arguments>)
allow(instance).to receive(:method).with(<arguments>)...
</>
stub on any instance# Old
Object.any_instance.stub(:method)
Object.any_instance.stub(:method).with(<arguments>)
Object.any_instance.stub(:method).with(<arguments>).and_...
# New
allow_any_instance_of(Object).to receive(:method)
allow_any_instance_of(Object).to receive(:method).with...
allow_any_instance_of(Object).to receive(:method).with...
</>
stub chain# Old
instance.stub_chain(:method_a, :method_b)
# New
allow(instance).to receive_message_chain(:method_a, :method_b)
</>
Method expectations# Old
instance.should_receive(:method)
instance.should_receive(:method).with(<arguments>)
# New
expect(instance).to receive(:method)
expect(instance).to receive(:method).with(<arguments>)
</>
...on any instance# Old
Object.any_instance.should_receive(:method)
Object.any_instance.should_receive(:method).with(<arguments>)
# New
expect_any_instance_of(Object).to receive(:method)
expect_any_instance_of(Object).to receive(:method).with...
Tools of trade
Skip it...
</>
Many ways to skip an example# Common way
#
it ‘stores 5 in its attribute’, skip: true do
# Giving a reason for the output
#
it ‘stores 5 in its attribute’, skip: ‘reason’ do
# Shortcut
#
skip ‘stores 5 in its attribute’ do
...or leave it pending
Pending should make sense
● When an example inside a pending block passes, RSpec will show an error
● This enforces that all pending blocks are pending for a good reason
Matchmakers
Cha-cha-chain
</>
Chaining# You can chain multiple ‘matchers’
#
expect(subject.attribute).
to start_with(‘hello’).and end_with(‘world’)
expect(subject.attribute).
to start_with(‘hello’).or start_with(‘goodbye’)
</>
The cooler way# You can chain multiple ‘matchers’
#
expect(subject.attribute).
to start_with(‘hello’) & end_with(‘world’)
expect(subject.attribute).
to start_with(‘hello’) | start_with(‘goodbye’)
Structural matching
</>
A matcher for structures# ‘match’ can validate Hash and Array object structures
#
let(:structure) { {key: :value, other_key: :other_value} }
expect(structure).
to match({key: :value, other_key: :other_value})
# Still works for strings and regexps as before
=~?
</>
Match arrays# =~ does not work anymore as an array’s content matcher
# Now
expect(ary).to contain_exactly(1, 2, 3)
# Also in its explicit version
expect(ary).to match_array([1, 2, 3])
be_yourself
</>
Cambios en el ser# ‘be_true’ becomes ‘be_truthy’
expect(true).to be_truthy
# ‘be_false’ becomes ‘be_falsey’ or ‘be_falsy’
expect(false).to be_falsey
expect(false).to be_falsy
# Having better semantics like in...
expect(nil).to be_falsey # instead of ‘be_false’
</>
Cambios en el ser
expect(nil).to be_false # Fails
expect(nil).to be_falsey # Passes
expect(‘hello world’).to be_true # Fails
expect(‘hello world’).to be_truthy # Passes
What’s goneis kind of gone
The fired matchers
● its
github.com/rspec/rspec-its● have, have_at_least, have_at_most
github.com/rspec/rspec-collection_matchers
Better Specs reviewed
Put some context
</>
Why, oh lord, why?it ‘stores 5 in its attribute if attribute b is 6 and
attribute c is 7’ do
subject.attribute_b = 6
subject.attribute_c = 7
expect(subject.attribute).to eq 5
end
</>
Why not just...?context ‘when attribute b is 6’ do
# ...
context ‘and attribute c is 7’ do
# ...
it ‘stores 5 in its attribute’ do
expect(subject.attribute).to eq 5
end
Analysis
● We can find good reasons not to do this:○ It’s more code○ It’s cumbersome○ I’m just a lazy dog
● RSpec is about documentation.● Documentation takes time.● Good documentation takes even more time.
IsolationVS
Combination
</>
This is not always a bad thingit 'sets the attribute' do
subject.very_expensive_loading
expect(subject.attribute).to be_kind_of(Fixnum)
expect(subject.attribute).to eq(5)
end
</>
...but sometimes it’s unnecessaryit 'stores a fixnum in its attribute' do
expect(subject.attribute).to be_kind_of(Fixnum)
end
it 'its attribute\’s fixnum is 5' do
expect(subject.attribute).to eq(5)
end
The right methodVS
The right result
</>
¿What should we test for?it 'sets the attribute to 5' do
expect(subject).to receive(:=).with(5) # Dramatization
subject.set_attribute(5)
end
# Instead of
it 'sets the attribute to 5' do
subject.set_attribute(5)
expect(subject.attribute).to eq 5
end
Analysis
● Not always unnecessary.● When it’s an expensive operations like the ones that
require access to an outside service, there’s no need to test for its results. (Assume it’s already tested)
● Don’t be scared to raise the coverage of the same method.
Change the subject
</>
Change the subject when you canit 'returns an array’ do
expect(subject.method).to be_kind_of(Array)
end
it 'returns an array that includes \’foo\’' do
expect(subject.method).to include(:foo)
end
it 'returns an array with something else' do
# ...
</>
Change the subject when you candescribe '#method\’s return value' do
subject { instance.method }
it 'is an array’ do
expect(subject).to be_kind_of(Array)
end
it 'includes \’foo\’' do
expect(subject).to include(:foo)
end
Let it be
</>
¡¡¡!!!before do
@some_value = :foo
@some_other_value = :boo
@yet_some_other_value = :cmon_bro
@again_and_again = :you_are_killing_me
end
</>
And its little brotherit 'does something’ do
value_a = :foo
value_b = :boo
value_c = :cmon_bro
value_d = :you_are_killing_me
expect(instance.
method(value_a, value_b, value_c, value_d)).
to_not raise_error
end
</>
A relief in sightlet(:value_a) { :foo }
let(:value_b) { :boo }
let(:value_c) { :cmon_bro }
let(:value_d) { :you_are_killing_me }
it 'does something’ do
expect(instance.
method(value_a, value_b, value_c, value_d)).
to_not raise_error
end
Analysis
● Reuse of variables (DRY)● Lazy loading● Readability (Good documentation)
Avoid creatinga monster
</>
Don’t get excited about createlet(:model) { create(:model) }
# ORLY?
</>
Don’t get excited about createlet(:model) { build(:model) }
Analysis
● create stores in the database.● I mean, it stores in the database.● Do we really need to persist?● build is new without save● The bad thing is that we have to run callbacks
manually or setting up our Factory to do so● Is that bad, anyway?
Stub when you have to
</>
Look what a pretty request# Let’s pretend ‘instance.call_api’ is a very expensive call
it 'parses the api response’ do
expect(subject.parser(instance.call_api)).
to be_kind_of(Hash)
end
</>
Look what a pretty requestbefore do
allow(instance).to receive(:call_api).
and_return(<valid response from api>)
end
it 'parses the api response’ do
expect(subject.parser(instance.call_api)).
to be_kind_of(Hash)
end
Learn to share
</>
Inherited behaviorclass Foo < Bar
# …
describe Foo do
it ‘serves drinks’
it ‘does whatever a foo does’
</>
Inherited behaviorclass Foo < Bar
# …
shared_examples_for Bar do
it ‘serves drinks’
end
describe Foo do
it_behaves_like Bar
it ‘does whatever a foo does’
end
Analysis
● Reuse of examples (DRY).● Consistent behavior across all subclasses.● The idea is to only verify what’s inherited; we don’t
want to test private stuff.
Agree to disagree
David Heinemeier Hansson, creator of Ruby on Rails
David Heinemeier Hansson, creator of Ruby on Rails
David Heinemeier Hansson, creator of Ruby on Rails
David Heinemeier Hansson, creator of Ruby on Rails
myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3betterspecs.orgrubyinside.com/dhh-offended-by-rspec-debate-4610.html
Sources
La fin