RSpec best practice
avoid using before and let
Bruce Li
You can write good test
cases without using
before and let
DRY app code
DAMP test code
Descriptive And Meaningful Phrases
RSpec.describe Admin::OrderController, type: :controller do
describe '#index' do
it 'assigns @orders' do
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in create(:user, :admin)
product = create(:product, price: 20)
order = create(:order, product: [product])
get :index
expect(assigns(:orders)).to include(order)
end
end
end
RSpec.describe Admin::OrderController, type: :controller do
describe '#index' do
it 'assigns @orders' do
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in create(:user, :admin)
product = create(:product, price: 20)
order = create(:order, products: [product])
get :index
expect(assigns(:orders)).to include(order)
end
end
end
RSpec.describe Admin::OrderController, type: :controller do
describe '#index' do
it 'assigns @orders' do
get :index
expect(assigns(:orders)).to include(order)
end
end
def sign_in_as_admin
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in create(:user, :admin)
end
def create_an_order
product = create(:product, price: 20)
create(:order, products: [product])
end
end
RSpec.describe Admin::OrderController, type: :controller do
describe '#index' do
it 'assigns @orders' do
sign_in_as_admin
order = create_an_order
get :index
expect(assigns(:orders)).to include(order)
end
end
def sign_in_as_admin
@request.env['devise.mapping'] = Devise.mappings[:user]
sign_in create(:user, :admin)
end
def create_an_order
product = create(:product, price: 20)
create(:order, products: [product])
end
end
NO
The hardest part…
learn this technique
convince other people
what's wrong with
before/let?
RSpec.describe User, type: :model do
# ...
describe '#watch' do
# ...
context 'restricted 21 movies' do
# ...
context 'teenager' do
# ...
it 'raises error' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
end
end
RSpec.describe User, type: :model do
let(:user) { create(:user, name: 'Uncle Lim', age: 50) }
describe '#watch' do
# ...
context 'restricted 21 movies' do
# ...
context 'teenager' do
# ...
it 'raises error' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
end
end
RSpec.describe User, type: :model do
let(:user) { create(:user, name: 'Uncle Lim', age: 50) }
describe '#watch' do
# ...
context 'restricted 21 movies' do
# ...
context 'teenager' do
let(:user) { create(:user, name: 'John', age: 20) }
it 'raises error' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
end
end
RSpec.describe User, type: :model do
let(:user) { create(:user, name: 'Uncle Lim', age: 50) }
describe '#watch' do
# ...
context 'restricted 21 movies' do
# ...
context 'teenager' do
let(:user) { create(:user, name: 'John', age: 20) }
before do
user.age += 2
end
it 'raises error' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
end
end
RSpec.describe User, type: :model do
let(:user) { create(:user, name: 'Uncle Lim', age: 50) }
describe '#watch' do
before { ... }
context 'restricted 21 movies' do
before { ... }
context 'teenager' do
let(:user) { create(:user, name: 'John', age: 20) }
before do
user.age += 2
end
it 'raises error' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
end
end
RSpec.describe Order, type: :model do
let(:item ) { create(:item, price: 100) }
let(:order) { create(:order, items: [item]) }
describe '#calculate' do
context 'when item/cart discount both applied' do
before do
order.apply_gst(0.8)
order.items.first.apply_discount(-10)
order.apply_cart_discount(-10)
order.calculate
end
it 'returns 100 * 1.08 - 10 - 10' do
expect { order.price }.to eq(88)
end
end
end
end
RSpec.describe Order, type: :model do
let(:item ) { create(:item, price: 100) }
let(:order) { create(:order, items: [item]) }
describe '#calculate' do
context 'when item/cart discount both applied' do
before do
order.apply_gst(0.8)
order.items.first.apply_discount(-10)
order.apply_cart_discount(-10)
order.apply_cash_back(-5)
order.calculate
end
it 'returns 100 * 1.08 - 10 - 10' do
expect { order.price }.to eq(88)
end
it 'returns 100 * 1.08 - 10 - 10 - 5' do
expect { order.price }.to eq(83)
end
end
end
RSpec.describe Order, type: :model do
let(:item ) { create(:item, price: 100) }
let(:order) { create(:order, items: [item]) }
describe '#calculate' do
context 'when item/cart discount both applied' do
before do
order.apply_gst(0.8)
order.items.first.apply_discount(-10)
order.apply_cart_discount(-10)
order.apply_cash_back(-5)
order.calculate
end
it 'returns 100 * 1.08 - 10 - 10' do
expect { order.price }.to eq(88)
end
it 'returns 100 * 1.08 - 10 - 10 - 5' do
expect { order.price }.to eq(83)
end
end
end
Why it matters
Why it matters
• If you have tight deadlines and limited engineer resource,
you will encounter all these problems
• Test is the safely net for refactoring
• When the test itself is a technical debt, you are walking a
tightrope without the net
Why it matters
• If you have tight deadlines and limited engineer resource,
you will encounter all these problems
• Test is the safely net for refactoring
• When the test itself is a technical debt, you are walking a
tightrope without the net
• Working on that kind of project is a death march
Why it matters
• If you have tight deadlines and limited engineer resource,
you will encounter all these problems

• Test is the safely net for refactoring

• When the test itself is a technical debt, you are walking a
tightrope without the net

• Working on that kind of project is a death march
Why it matters
• If you have tight deadlines and limited engineer resource,
you will encounter all these problems

• Test is the safely net for refactoring

• When the test itself is a technical debt, you are walking a
tightrope without the net

• Working on that kind of project is a death march
Why it matters
• If you have tight deadlines and limited engineer resource,
you will encounter all these problems

• Test is the safely net for refactoring

• When the test itself is a technical debt, you are walking a
tightrope without the net

• Working on that kind of project is a death march
Good test? Not yet
One test, one topic
describe 'User' do
describe '#allow_watch?' do
it 'returns true if user John is adult' do
user = create(:user, name: 'John Lim', age: 22)
r21_movie = create(:movie, rate: 'r21')
expect(user.first_name).to eq('John')
expect(user.allow_watch?(r21_movie)).to be_true
end
end
end
One test, one topic
describe 'User' do
describe '#allow_watch?' do
it 'returns true if user John is adult' do
user = create(:user, name: 'John Lim', age: 22)
r21_movie = create(:movie, rate: 'r21')
expect(user.first_name).to eq('John')
expect(user.allow_watch?(r21_movie)).to be_true
end
end
end
One test, one topic
describe 'User' do
describe '#allow_watch?' do
it 'returns true if user is adult' do
user = create(:user, age: 22)
r21_movie = create(:movie, rate: 'r21')
expect(user.allow_watch?(r21_movie)).to be_true
end
end
end
Exact, important detail to test
describe 'User' do
let!(:movie) { create(:movie, rate: 'pg') }
describe 'first_name' do
it 'returns first part of the full name' do
user = build(
:user,
name: 'John Lim',
age: 20,
active: true
# ...
)
expect(user.first_name).to eq('John')
end
end
end
Exact, important detail to test
describe 'User' do
let!(:movie) { create(:movie, rate: 'pg') }
describe 'first_name' do
it 'returns first part of the full name' do
user = build(
:user,
name: 'John Lim',
age: 20,
active: true
# ...
)
expect(user.first_name).to eq('John')
end
end
end
Exact, important detail to test
describe 'User' do
let!(:movie) { create(:movie, rate: 'pg') }
describe 'first_name' do
it 'returns first part of the full name' do
user = build(
:user,
name: 'John Lim',
age: 20,
active: true
# ...
)
expect(user.first_name).to eq('John')
end
end
end
Exact, important detail to test
describe 'User' do
describe 'first_name' do
it 'is the first part of the name' do
user = build(:user)
expect(user.first_name).to eq('John')
end
end
end
Exact, important detail to test
describe 'User' do
describe 'first_name' do
it 'is the first part of the name' do
user = build(:user, name: 'John Lim')
expect(user.first_name).to eq('John')
end
end
end
Exact, important detail to test
describe 'User' do
describe 'first_name' do
it 'is the first part of the name' do
user = build(:user, name: 'John Lim')
expect(user.first_name).to eq('John')
end
end
end
Flatten context
RSpec.describe User, type: :model do
# ...
describe '#watch' do
# ...
context 'restricted 21 movies' do
# ...
context 'teenager' do
# ...
it 'raises error' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
end
end
Flatten context
RSpec.describe User, type: :model do
# ...
describe '#watch' do
# ...
it 'raises error when teenager watch R21 movies' do
# ...
expect{ user.watch(r21_movie) }.to raise_error
end
end
end
Four-Phase Test
describe Class do
context '#method_name' do
it 'verbs ooo xxx...' do
preparation
exercise
verify (expects)
teardown
end
end
end
Use documentation format
User
#first_name
returns first part of the full name
#last_name
returns last part of the full name
#watch
raises exception if the user is not adult
does not raise exception if the user is adult
...
(Add "--color --format documentation" to your .rspec)
Four-Phase Test
describe Class do
context '#method_name' do
it 'verbs ooo xxx...' do
preparation
exercise
verify (expects)
teardown
end
end
end
Use documentation format
User
#first_name
returns first part of the full name
#last_name
returns last part of the full name
#watch
raises exception if the user is not adult
does not raise exception if the user is adult
...
(Add "--color --format documentation" to your .rspec)
How do you know if that work
How do you know if that work
How do you know if that work
How do you know if that work
1 hour
Conclusion
<commercial break>
<commercial break>
We are hiring
We are hiring
awesome_rails_console
awesome_rails_console
awesome_rails_console
awesome_rails_console
rails g awesome_rails_console:install
gem 'awesome_rails_console'
# in Gemfile
# in Terminal app
downloads-folder-cleaner
downloads-folder-cleaner
Ruby and Rails Best Practices
e-book
https://goo.gl/ofH6MG
Conclusion
Conclusion
• It’s simple, easy to read, easy to change
and it works everywhere
Conclusion
• It’s simple, easy to read, easy to change
and it works everywhere
• Writing good test cases without using
before and let is possible
Conclusion
• It’s simple, easy to read, easy to change
and it works everywhere
• Writing good test cases without using
before and let is possible
• Reducing WTF count from dozens to a
few is possible
Conclusion
• It’s simple, easy to read, easy to change
and it works everywhere
• Writing good test cases without using
before and let is possible
• Reducing WTF count from dozens to a
few is possible
• Let's write better tests and be happier
Thank you!
• In case you missed it:

• awesome_rails_console

• "downloads-folder-cleaner"

• e-book: https://goo.gl/ofH6MG

• Feedbacks welcome!

• Leave comment in Meetup event page

• ascendbruce (twitter / facebook / gmail)
additional notes
• To summarise the problem with before and let:

• People are tempting to write test in a way that each examples are easy to
pollute each other, which make it hard to change.

• Some people might come up with "creative" ways to workaround that, which
make it hard to read.

• When you are under time pressure. It's really hard to stop those things from
happening.

• There are trade-offs in this style too. But if you place "ease of maintenance" at
top priority. This style is going to help a lot.

• You can use before/let for some parts when it becomes a bottleneck.

• This idea is not original by me. I learned it from thoughtbot.

RSpec best practice - avoid using before and let

  • 1.
    RSpec best practice avoidusing before and let Bruce Li
  • 2.
    You can writegood test cases without using before and let
  • 3.
    DRY app code DAMPtest code Descriptive And Meaningful Phrases
  • 4.
    RSpec.describe Admin::OrderController, type::controller do describe '#index' do it 'assigns @orders' do @request.env['devise.mapping'] = Devise.mappings[:user] sign_in create(:user, :admin) product = create(:product, price: 20) order = create(:order, product: [product]) get :index expect(assigns(:orders)).to include(order) end end end
  • 5.
    RSpec.describe Admin::OrderController, type::controller do describe '#index' do it 'assigns @orders' do @request.env['devise.mapping'] = Devise.mappings[:user] sign_in create(:user, :admin) product = create(:product, price: 20) order = create(:order, products: [product]) get :index expect(assigns(:orders)).to include(order) end end end
  • 6.
    RSpec.describe Admin::OrderController, type::controller do describe '#index' do it 'assigns @orders' do get :index expect(assigns(:orders)).to include(order) end end def sign_in_as_admin @request.env['devise.mapping'] = Devise.mappings[:user] sign_in create(:user, :admin) end def create_an_order product = create(:product, price: 20) create(:order, products: [product]) end end
  • 7.
    RSpec.describe Admin::OrderController, type::controller do describe '#index' do it 'assigns @orders' do sign_in_as_admin order = create_an_order get :index expect(assigns(:orders)).to include(order) end end def sign_in_as_admin @request.env['devise.mapping'] = Devise.mappings[:user] sign_in create(:user, :admin) end def create_an_order product = create(:product, price: 20) create(:order, products: [product]) end end
  • 10.
  • 11.
    The hardest part… learnthis technique convince other people
  • 12.
  • 13.
    RSpec.describe User, type::model do # ... describe '#watch' do # ... context 'restricted 21 movies' do # ... context 'teenager' do # ... it 'raises error' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end end end
  • 14.
    RSpec.describe User, type::model do let(:user) { create(:user, name: 'Uncle Lim', age: 50) } describe '#watch' do # ... context 'restricted 21 movies' do # ... context 'teenager' do # ... it 'raises error' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end end end
  • 15.
    RSpec.describe User, type::model do let(:user) { create(:user, name: 'Uncle Lim', age: 50) } describe '#watch' do # ... context 'restricted 21 movies' do # ... context 'teenager' do let(:user) { create(:user, name: 'John', age: 20) } it 'raises error' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end end end
  • 16.
    RSpec.describe User, type::model do let(:user) { create(:user, name: 'Uncle Lim', age: 50) } describe '#watch' do # ... context 'restricted 21 movies' do # ... context 'teenager' do let(:user) { create(:user, name: 'John', age: 20) } before do user.age += 2 end it 'raises error' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end end end
  • 17.
    RSpec.describe User, type::model do let(:user) { create(:user, name: 'Uncle Lim', age: 50) } describe '#watch' do before { ... } context 'restricted 21 movies' do before { ... } context 'teenager' do let(:user) { create(:user, name: 'John', age: 20) } before do user.age += 2 end it 'raises error' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end end end
  • 18.
    RSpec.describe Order, type::model do let(:item ) { create(:item, price: 100) } let(:order) { create(:order, items: [item]) } describe '#calculate' do context 'when item/cart discount both applied' do before do order.apply_gst(0.8) order.items.first.apply_discount(-10) order.apply_cart_discount(-10) order.calculate end it 'returns 100 * 1.08 - 10 - 10' do expect { order.price }.to eq(88) end end end end
  • 19.
    RSpec.describe Order, type::model do let(:item ) { create(:item, price: 100) } let(:order) { create(:order, items: [item]) } describe '#calculate' do context 'when item/cart discount both applied' do before do order.apply_gst(0.8) order.items.first.apply_discount(-10) order.apply_cart_discount(-10) order.apply_cash_back(-5) order.calculate end it 'returns 100 * 1.08 - 10 - 10' do expect { order.price }.to eq(88) end it 'returns 100 * 1.08 - 10 - 10 - 5' do expect { order.price }.to eq(83) end end end
  • 20.
    RSpec.describe Order, type::model do let(:item ) { create(:item, price: 100) } let(:order) { create(:order, items: [item]) } describe '#calculate' do context 'when item/cart discount both applied' do before do order.apply_gst(0.8) order.items.first.apply_discount(-10) order.apply_cart_discount(-10) order.apply_cash_back(-5) order.calculate end it 'returns 100 * 1.08 - 10 - 10' do expect { order.price }.to eq(88) end it 'returns 100 * 1.08 - 10 - 10 - 5' do expect { order.price }.to eq(83) end end end
  • 21.
  • 22.
    Why it matters •If you have tight deadlines and limited engineer resource, you will encounter all these problems • Test is the safely net for refactoring • When the test itself is a technical debt, you are walking a tightrope without the net
  • 23.
    Why it matters •If you have tight deadlines and limited engineer resource, you will encounter all these problems • Test is the safely net for refactoring • When the test itself is a technical debt, you are walking a tightrope without the net • Working on that kind of project is a death march
  • 24.
    Why it matters •If you have tight deadlines and limited engineer resource, you will encounter all these problems • Test is the safely net for refactoring • When the test itself is a technical debt, you are walking a tightrope without the net • Working on that kind of project is a death march
  • 25.
    Why it matters •If you have tight deadlines and limited engineer resource, you will encounter all these problems • Test is the safely net for refactoring • When the test itself is a technical debt, you are walking a tightrope without the net • Working on that kind of project is a death march
  • 26.
    Why it matters •If you have tight deadlines and limited engineer resource, you will encounter all these problems • Test is the safely net for refactoring • When the test itself is a technical debt, you are walking a tightrope without the net • Working on that kind of project is a death march
  • 27.
  • 28.
    One test, onetopic describe 'User' do describe '#allow_watch?' do it 'returns true if user John is adult' do user = create(:user, name: 'John Lim', age: 22) r21_movie = create(:movie, rate: 'r21') expect(user.first_name).to eq('John') expect(user.allow_watch?(r21_movie)).to be_true end end end
  • 29.
    One test, onetopic describe 'User' do describe '#allow_watch?' do it 'returns true if user John is adult' do user = create(:user, name: 'John Lim', age: 22) r21_movie = create(:movie, rate: 'r21') expect(user.first_name).to eq('John') expect(user.allow_watch?(r21_movie)).to be_true end end end
  • 30.
    One test, onetopic describe 'User' do describe '#allow_watch?' do it 'returns true if user is adult' do user = create(:user, age: 22) r21_movie = create(:movie, rate: 'r21') expect(user.allow_watch?(r21_movie)).to be_true end end end
  • 31.
    Exact, important detailto test describe 'User' do let!(:movie) { create(:movie, rate: 'pg') } describe 'first_name' do it 'returns first part of the full name' do user = build( :user, name: 'John Lim', age: 20, active: true # ... ) expect(user.first_name).to eq('John') end end end
  • 32.
    Exact, important detailto test describe 'User' do let!(:movie) { create(:movie, rate: 'pg') } describe 'first_name' do it 'returns first part of the full name' do user = build( :user, name: 'John Lim', age: 20, active: true # ... ) expect(user.first_name).to eq('John') end end end
  • 33.
    Exact, important detailto test describe 'User' do let!(:movie) { create(:movie, rate: 'pg') } describe 'first_name' do it 'returns first part of the full name' do user = build( :user, name: 'John Lim', age: 20, active: true # ... ) expect(user.first_name).to eq('John') end end end
  • 34.
    Exact, important detailto test describe 'User' do describe 'first_name' do it 'is the first part of the name' do user = build(:user) expect(user.first_name).to eq('John') end end end
  • 35.
    Exact, important detailto test describe 'User' do describe 'first_name' do it 'is the first part of the name' do user = build(:user, name: 'John Lim') expect(user.first_name).to eq('John') end end end
  • 36.
    Exact, important detailto test describe 'User' do describe 'first_name' do it 'is the first part of the name' do user = build(:user, name: 'John Lim') expect(user.first_name).to eq('John') end end end
  • 37.
    Flatten context RSpec.describe User,type: :model do # ... describe '#watch' do # ... context 'restricted 21 movies' do # ... context 'teenager' do # ... it 'raises error' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end end end
  • 38.
    Flatten context RSpec.describe User,type: :model do # ... describe '#watch' do # ... it 'raises error when teenager watch R21 movies' do # ... expect{ user.watch(r21_movie) }.to raise_error end end end
  • 39.
    Four-Phase Test describe Classdo context '#method_name' do it 'verbs ooo xxx...' do preparation exercise verify (expects) teardown end end end
  • 40.
    Use documentation format User #first_name returnsfirst part of the full name #last_name returns last part of the full name #watch raises exception if the user is not adult does not raise exception if the user is adult ... (Add "--color --format documentation" to your .rspec)
  • 41.
    Four-Phase Test describe Classdo context '#method_name' do it 'verbs ooo xxx...' do preparation exercise verify (expects) teardown end end end
  • 42.
    Use documentation format User #first_name returnsfirst part of the full name #last_name returns last part of the full name #watch raises exception if the user is not adult does not raise exception if the user is adult ... (Add "--color --format documentation" to your .rspec)
  • 43.
    How do youknow if that work
  • 44.
    How do youknow if that work
  • 45.
    How do youknow if that work
  • 46.
    How do youknow if that work 1 hour
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
    awesome_rails_console rails g awesome_rails_console:install gem'awesome_rails_console' # in Gemfile # in Terminal app
  • 57.
  • 58.
  • 59.
    Ruby and RailsBest Practices e-book https://goo.gl/ofH6MG
  • 60.
  • 61.
    Conclusion • It’s simple,easy to read, easy to change and it works everywhere
  • 62.
    Conclusion • It’s simple,easy to read, easy to change and it works everywhere • Writing good test cases without using before and let is possible
  • 63.
    Conclusion • It’s simple,easy to read, easy to change and it works everywhere • Writing good test cases without using before and let is possible • Reducing WTF count from dozens to a few is possible
  • 64.
    Conclusion • It’s simple,easy to read, easy to change and it works everywhere • Writing good test cases without using before and let is possible • Reducing WTF count from dozens to a few is possible • Let's write better tests and be happier
  • 65.
    Thank you! • Incase you missed it: • awesome_rails_console • "downloads-folder-cleaner" • e-book: https://goo.gl/ofH6MG • Feedbacks welcome! • Leave comment in Meetup event page • ascendbruce (twitter / facebook / gmail)
  • 66.
    additional notes • Tosummarise the problem with before and let: • People are tempting to write test in a way that each examples are easy to pollute each other, which make it hard to change. • Some people might come up with "creative" ways to workaround that, which make it hard to read. • When you are under time pressure. It's really hard to stop those things from happening. • There are trade-offs in this style too. But if you place "ease of maintenance" at top priority. This style is going to help a lot. • You can use before/let for some parts when it becomes a bottleneck. • This idea is not original by me. I learned it from thoughtbot.