Slide 1

Slide 1 text

Writing Expressive Tests with RSpec Wei Lu! @luweidewei

Slide 2

Slide 2 text

Disclaimer

Slide 3

Slide 3 text

Rails Alert

Slide 4

Slide 4 text

Agenda • let! • subject! • Caveats! • Tricks

Slide 5

Slide 5 text

Let 1 describe '#process' do! 2 let(:card) { CreditCard.new type: type }! 3 ! 4 context 'when type is visa' do! 5 let(:type) { :visa }! 6 ! 7 # examples...! 8 end! 9 end!

Slide 6

Slide 6 text

`let` vs. @instance_variable • correctness: avoid coincidental passing tests! • performance: lazy evaluation + memoization

Slide 7

Slide 7 text

1 describe '#locate' do! 2 context 'when not found' do! 3 before { @card = CreditCard.locate 123 }! 4 ! 5 it { expect(@car).to be_nil }! 6 end! 7 ! 8 context 'when found'! 9 end! Coincidental passing tests (>_<) spawn into existence

Slide 8

Slide 8 text

`let` vs. local_variable • why not local_variables?! • not lazily evaluated but does it matter?

Slide 9

Slide 9 text

Model scope specs 1 describe ".valid" do! 2 let!(:valid_card) { create(:credit_card, expires: 2.days.ago) }! 3 let!(:invalid_card) { create(:credit_card, expires: 2.months.from_now) }! 4 ! 5 it 'returns valid cards' do! 6 expect(described_class.valid).to_not include(invalid_card)! 7 expect(described_class.valid).to include(valid_card)! 8 end! 9 end! 10 ! (^o^)

Slide 10

Slide 10 text

Model scope specs 1 describe ".valid" do! 2 it 'returns valid cards' do! 3 valid_card = create(:credit_card, expires_at: 2.days.ago)! 4 invalid_card = create(:credit_card, expires_at: 2.month.from_now)! 5 ! 6 expect(described_class.valid).to_not include(invalid_card)! 7 expect(described_class.valid).to include(valid_card)! 8 end! 9 end! 10 ! (^o^)

Slide 11

Slide 11 text

Integration specs 1 feature "User can purchase an item" do! 2 scenario "with a valid credit card" do! 3 user = create :user! 4 card = create :credit_card, owner: user, balance: initial_balance! 5 initial_balance = Money.new(10000, 'SGD')! 6 item = create :item, price: item_price! 7 item_price = Money.new(200, 'SGD')! 8 ! 9 sign_in user! 10 visit root_path! 11 click_on 'Catalog'! 12 #...! 13 end! 14 end! 15 ! (^o^)

Slide 12

Slide 12 text

`let` what?

Slide 13

Slide 13 text

1 describe '#process' do! 2 context 'when type is visa' do! 3 let(:card) { CreditCard.new type: :visa }! 4 ! 5 # examples...! 6 end! 7 ! 8 context 'when type is mastercard' do! 9 let(:card) { CreditCard.new type: :mastercard }! 10 ! 11 # examples...! 12 end! 13 end! (>_<)

Slide 14

Slide 14 text

let the smallest thing possible 1 describe '#process' do! 2 let(:card) { CreditCard.new type: type }! 3 ! 4 context 'when type is visa' do! 5 let(:type) { :visa }! 6 ! 7 # examples...! 8 end! 9 ! 10 context 'when type is mastercard' do! 11 let(:type) { :mastercard }! 12 ! 13 # examples...! 14 end! 15 end! (^o^)

Slide 15

Slide 15 text

`let`s get lost in nested contexts describe '#a_method' do! context 'scenario 1' do! context 'sub-scenario 1' do! context 'sub-sub-scenario 1' do! #...! end! context 'sub-sub-scenario 2' do! #...! end! end! ! context 'sub-scenario 2' do! context 'sub-sub-scenario 1' do! #...! end! end! end! end

Slide 16

Slide 16 text

`let`s get lost in nested contexts • It’s a sign....
 it’s time to break up

Slide 17

Slide 17 text

`let!`s get lost in `let`s • Lexically separate let and let!! • Consider using `before` as an alternative

Slide 18

Slide 18 text

1 feature "User can purchase an item" do! 2 let(:user) { create :user }! 3 let!(:card) { create :credit_card, owner: user, balance: initial_balance }! 4 let(:initial_balance) { Money.new(10000, 'SGD') }! 5 let!(:item) { create :item, price: item_price }! 6 let(:item_price) { Money.new(200, 'SGD') }! 7 ! 8 scenario "with a valid credit card"! 9 end! 10 !

Slide 19

Slide 19 text

Lexical separation 1 feature "User can purchase an item" do! 2 let!(:card) { create :credit_card, owner: user, balance: initial_balance 3 let!(:item) { create :item, price: item_price }! 4 ! 5 let(:initial_balance) { Money.new(10000, 'SGD') }! 6 let(:item_price) { Money.new(200, 'SGD') }! 7 let(:user) { create :user }! 8 ! 9 scenario "with a valid credit card"! 10 end! 11 ! (>_<)

Slide 20

Slide 20 text

`before` as an alternative 1 feature "User can purchase an item" do! 2 let(:initial_balance) { Money.new(10000, 'SGD') }! 3 let(:item_price) { Money.new(200, 'SGD') }! 4 let(:user) { create :user }! 5 ! 6 before do! 7 create :credit_card, owner: user, balance: initial_balance! 8 create :item, price: item_price! 9 end! 10 ! 11 scenario "with a valid credit card"! 12 end! 13 ! (^o^)

Slide 21

Slide 21 text

`let`s get abused

Slide 22

Slide 22 text

Mysterious local variables in shared example 1 shared_examples "a collection object" do! 2 describe "<<" do! 3 it "adds objects to the end of the collection" do! 4 collection << 1! 5 collection << 2! 6 collection.to_a.should eq([1,2])! 7 end! 8 end! 9 end! 10 ! 11 it_behaves_like "a collection object" do! 12 let(:collection) { Array.new }! 13 end! (>_<)

Slide 23

Slide 23 text

1 shared_examples "a collection object" do |collection|! 2 describe "<<" do! 3 it "adds objects to the end of the collection" do! 4 collection << 1! 5 collection << 2! 6 collection.to_a.should eq([1,2])! 7 end! 8 end! 9 end! 10 ! 11 it_behaves_like "a collection object", Array.new! (^o^)

Slide 24

Slide 24 text

subject

Slide 25

Slide 25 text

# @note `subject` was contributed by Joe Ferris ! # to support the one-liner syntax embraced by ! # shoulda matchers

Slide 26

Slide 26 text

subject 1 describe CreditCard do! 2 it { should belong_to :address }! 3 it { should accept_nested_attributes_for :address }! 4 ! 5 describe 'validations' do! 6 it { should validate_presence_of :name }! 7 it { should validate_presence_of :card_number }! 8 end! 9 end!

Slide 27

Slide 27 text

subject 1 describe CreditCard do! 2 it { should belong_to :address }! 3 it { should accept_nested_attributes_for :address }! 4 ! 5 describe 'validations' do! 6 it { should validate_presence_of :name }! 7 it { should validate_presence_of :card_number }! 8 end! 9 end!

Slide 28

Slide 28 text

why? Why should I use subject? ! • Readability ! • DRY

Slide 29

Slide 29 text

without subject 1 describe CreditCard do! 2 it { CreditCard.new.should belong_to :address }! 3 it { CreditCard.new.should accept_nested_attributes_for :address }! 4 ! 5 describe 'validations' do! 6 it { CreditCard.new.should validate_presence_of :name }! 7 it { CreditCard.new.should validate_presence_of :card_number }! 8 end! 9 end!

Slide 30

Slide 30 text

DRY it up with @instance_variable 1 describe CreditCard do! 2 before { @credit_card = described_class.new }! 3 ! 4 it { @credit_card.should belong_to :address }! 5 it { @credit_card.should accept_nested_attributes_for :address }! 6 ! 7 describe 'validations' do! 8 it { @credit_card.should validate_presence_of :name }! 9 it { @credit_card.should validate_presence_of :card_number }! 10 end! 11 end!

Slide 31

Slide 31 text

1 describe CreditCard do! 2 let(:credit_card) { CreditCard.new }! 3 ! 4 it { credit_card.should belong_to :address }! 5 it { credit_card.should accept_nested_attributes_for :address }! 6 ! 7 describe 'validations' do! 8 it { credit_card.should validate_presence_of :name }! 9 it { credit_card.should validate_presence_of :card_number }! 10 end! 11 end! DRY it up with let

Slide 32

Slide 32 text

Implicit subject 1 describe CreditCard do! 2 it { should belong_to :address }! 3 it { should accept_nested_attributes_for :address }! 4 ! 5 describe 'validations' do! 6 it { should validate_presence_of :name }! 7 it { should validate_presence_of :card_number }! 8 end! 9 end!

Slide 33

Slide 33 text

Explicit subject 1 describe CreditCard do! 2 subject do! 3 CreditCard.new name: 'Ann', card_number: 1234! 4 end! 5 ! 6 it { should be_valid }! 7 end!

Slide 34

Slide 34 text

1 describe CreditCard do! 2 subject do! 3 CreditCard.new name: 'Ann', card_number: 1234! 4 end! 5 ! 6 it { should be_valid }! 7 ! 8 describe '#balance' do! 9 subject { CreditCard.new.balance }! 10 ! 11 it { should eq(0) }! 12 end! 13 ! 14 describe '#expired?' do! 15 subject { CreditCard.new(expires: '2012-01') }! 16 ! 17 it { expect(subject).to be_expired }! 18 end! 19 end! Ouch

Slide 35

Slide 35 text

1 describe CreditCard do! 2 subject do! 3 CreditCard.new name: 'Ann', card_number: 1234! 4 end! 5 ! 6 it { should be_valid }! 7 ! 8 describe '#balance' do! 9 its(:balance) { should eq(0) }! 10 end! 11 ! 12 describe '#expired?' do! 13 subject { CreditCard.new(expires: '2012-01') }! 14 ! 15 it { should be_expired }! 16 end! 17 end!

Slide 36

Slide 36 text

Named subject http://gifstumblr.com/images/thats-not-my-name_486.gif

Slide 37

Slide 37 text

Spec Tasting

Slide 38

Slide 38 text

1 describe CreditCardsController do! 2 ! 3 describe 'GET new' do! 4 it 'responses with status 200' do! 5 get :new! 6 expect(response.status).to eq(200)! 7 end! 8 ! 9 it 'assigns credit_card with an address' do! 10 get :new! 11 credit_card = assigns[:credit_card]! 12 expect(credit_card).to be_a(CreditCard)! 13 expect(credit_card.address).to be_an(Address)! 14 end! 15 end! 16 ! 17 end!

Slide 39

Slide 39 text

1 describe 'POST create' do! 2 it 'creates a credit card' do! 3 expect {! 4 post :create, credit_card: {! 5 name: "Alice Bob",! 6 card_number: "9807-8764-8274-4726",! 7 cvv: "999",! 8 expires: "0916",! 9 address_attributes: {! 10 line_1: "53 Craig Rd",! 11 line_2: "Singapore",! 12 line_3: "",! 13 zip_code: "097410",! 14 country_code: "Singapore"}! 15 }! 16 }.to change(CreditCard, :count).by(1)! 17 end! 18 ! 19 it 'assigns credit_card' do! 20 post :create, credit_card: {! 21 name: "Alice Bob",! 22 card_number: "9807-8764-8274-4726",! 23 cvv: "999",! 24 expires: "0916",! 25 address_attributes: {!

Slide 40

Slide 40 text

25 address_attributes: {! 26 line_1: "53 Craig Rd",! 27 line_2: "Singapore",! 28 line_3: "",! 29 zip_code: "097410",! 30 country_code: "Singapore"}! 31 }! 32 expect(assigns[:credit_card]).to eq(CreditCard.last)! 33 end! 34 ! 35 it 'redirects to show' do! 36 post :create, credit_card: {! 37 name: "Alice Bob",! 38 card_number: "9807-8764-8274-4726",! 39 cvv: "999",! 40 expires: "0916",! 41 address_attributes: {! 42 line_1: "53 Craig Rd",! 43 line_2: "Singapore",! 44 line_3: "",! 45 zip_code: "097410",! 46 country_code: "Singapore"}! 47 }! 48 expect(response).to redirect_to(credit_card_path CreditCard.last)! 49 end! 50 end!

Slide 41

Slide 41 text

1 describe CreditCardsController do! 2 ! 3 # refactoring attempt #1! 4 describe 'GET new' do! 5 subject(:do_request) { get :new }! 6 ! 7 it { expect(do_request.status).to eq(200) }! 8 it { expect(assigns[:credit_card]).to be_a(CreditCard) }! 9 it { expect(assigns[:credit_card].address).to be_an(Address) }! 10 end! 11 ! 12 end!

Slide 42

Slide 42 text

1 describe CreditCardsController do! 2 ! 3 # refactoring attempt #1.5! 4 describe 'GET new' do! 5 subject(:do_request) { get :new }! 6 ! 7 it { expect(do_request.status).to eq(200) }! 8 it { do_request; expect(assigns[:credit_card]).to be_a(CreditCard) }! 9 it { do_request; expect(assigns[:credit_card].address).to be_an(Address) } 10 end! 11 ! 12 end!

Slide 43

Slide 43 text

1 describe CreditCardsController do! 2 ! 3 # refactoring attempt #2! 4 describe 'GET new' do! 5 subject(:do_request) { get :new }! 6 ! 7 its(:status) { should eq(200) }! 8 ! 9 it 'assigns credit card with an address' do! 10 do_request! 11 credit_card = assigns[:credit_card]! 12 expect(credit_card).to be_a(CreditCard)! 13 expect(credit_card.address).to be_an(Address)! 14 end! 15 end! 16 ! 17 end!

Slide 44

Slide 44 text

1 describe CreditCardsController do! 2 ! 3 # refactoring attempt #3! 4 describe 'GET new' do! 5 before { get :new }! 6 ! 7 it { expect(response.status).to eq(200) }! 8 ! 9 it 'assigns credit card with an address' do! 10 credit_card = assigns[:credit_card]! 11 expect(credit_card).to be_a(CreditCard)! 12 expect(credit_card.address).to be_an(Address)! 13 end! 14 end! 15 ! 16 end!

Slide 45

Slide 45 text

Moving forward... and a bit beyond subject

Slide 46

Slide 46 text

`expect` vs. `should` syntax # expect syntax! expect(response.status).to eq(200)! ! # should syntax! response.status.should eq(200)!

Slide 47

Slide 47 text

choose one!  ...as long as it’s not `should`

Slide 48

Slide 48 text

with only `expect` syntax enabled # should syntax! before { get :new }! it { response.should be_success }! ! # should without global monkey patching! subject { get :new }! it { should be_success }! (>_<) (^o^)

Slide 49

Slide 49 text

1 RSpec.configure do |config|! 2 config.expect_with :rspec do |c|! 3 c.syntax = :expect! 4 # or! 5 c.syntax = :should! 6 # or! 7 c.syntax = [:should, :expect]! 8 # default, enables both `expect` and `should` (with warning)! 9 end! 10 end!

Slide 50

Slide 50 text

RSpec 3: `its` no more it’s a gem

Slide 51

Slide 51 text

1 # spec/models/credit_card_spec.rb! 2 describe CreditCard do! 3 its(:balance) { should eq(0) }! 4 end $ rspec spec/models/credit_card_spec.rb -fd! CreditCard! balance! should eq 0! ! Finished in 0.01874 seconds! 1 example, 0 failures!

Slide 52

Slide 52 text

Caveats #RSpecFail http://murderbymedia.files.wordpress.com/2013/12/building.gif

Slide 53

Slide 53 text

around 1 around do |example|! 2 do_some_stuff_before! 3 example.run! 4 do_some_stuff_after! 5 end! may not execute if example raises an error

Slide 54

Slide 54 text

Stubborn controller specs describe CreditCardsController do! ! describe 'GET new' do! subject(:do_request) { get :new }! ! its(:status) { should eq(200) }! ! it { expect(assigns[:credit_card]).to be_a(CreditCard) }! end! ! end! Y U no pass?

Slide 55

Slide 55 text

1 # spec/support/rspec-rails/controller-example-group.rb! 2 module RSpec::Rails! 3 module ControllerExampleGroup! 4 ! 5 def process *args! 6 @requested = true! 7 super! 8 end! 9 ! 10 def self.included(base)! 11 base.class_eval do! 12 after do! 13 unless @requested! 14 puts 'WARNING: No request method invoked.! 15 Have you forgotten to make a request?'! 16 end! 17 end! 18 end! 19 end! 20 ! 21 end! 22 end! https://gist.github.com/weilu/7104353

Slide 56

Slide 56 text

change expect {! do_request! }.to change(user, :location).to('SG')! expect {! do_request! }.to change{ user.reload.location }.to('SG')!

Slide 57

Slide 57 text

Tricks http://stream1.gifsoup.com/view/442153/surprised-kitty-o.gif

Slide 58

Slide 58 text

1 describe '#functional?' do! 2 let(:card) { stub_model CreditCard }! 3 ! 4 context 'when the card is valid' do! 5 before { card.stub(valid?: true) }! 6 ! 7 context 'when overdraft' do! 8 before { card.stub(overdraft?: true) }! 9 ! 10 it { expect(card).not_to be_functional}! 11 end! 12 ! 13 context 'when not overdraft' do! 14 before { card.stub(overdraft?: false) }! 15 ! 16 it { expect(card).to be_functional}! 17 end! 18 end! 19 ! state permutations

Slide 59

Slide 59 text

12 ! 13 context 'when not overdraft' do! 14 before { card.stub(overdraft?: false) }! 15 ! 16 it { expect(card).to be_functional}! 17 end! 18 end! 19 ! 20 context 'when the card is not valid' do! 21 before { card.stub(valid?: true) }! 22 ! 23 context 'when overdraft' do! 24 before { card.stub(overdraft?: true) }! 25 ! 26 it { expect(card).not_to be_functional}! 27 end! 28 ! 29 context 'when not overdraft' do! 30 before { card.stub(overdraft?: false) }! 31 ! 32 it { expect(card).not_to be_functional}! 33 end! 34 end! 35 end! state permutations

Slide 60

Slide 60 text

truth table 1 describe '#functional?' do! 2 let(:card) { stub_model CreditCard }! 3 ! 4 [! 5 [ true , true , false ],! 6 [ true , false , true ],! 7 [ false , true , false ],! 8 [ false , false , false ]! 9 ].each do | valid , overdraft , functional |! 10 ! 11 context "when card valid: #{valid}, and overdraft: #{overdraft}" do! 12 before do! 13 card.stub(valid?: valid, overdraft?: overdraft)! 14 end! 15 ! 16 it "functional should be #{functional}" do! 17 expect(card.functional?).to eq(functional)! 18 end! 19 end! 20 end! 21 end!

Slide 61

Slide 61 text

`require spec_helper` no more # .rspec! --require spec_helper

Slide 62

Slide 62 text

run multiple specs $ rspec spec/my_spec.rb:12:25

Slide 63

Slide 63 text

`tag: true` no more # spec_helper.rb! RSpec.configure do |c|! c.treat_symbols_as_metadata_keys_with_true_values = true! end! it 'works', :focus do! expect(true).to be_true! end!

Slide 64

Slide 64 text

References • https://www.relishapp.com/rspec/rspec-core/v/2-14/docs/subject! • http://benscheirman.com/2011/05/dry-up-your-rspec-files-with-subject-let-blocks/! • https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/memoized_helpers.rb! • http://blog.davidchelimsky.net/blog/2012/05/14/spec-smell-explicit-use-of-subject/! • http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax! • https://github.com/rspec/rspec-expectations/blob/master/Should.md! • https://gist.github.com/myronmarston/4503509! • http://betterspecs.org/! • http://vimeo.com/50185518! • http://stackoverflow.com/questions/11006888/testing-how-to-focus-on-behavior-instead-of-implementation- without-losing-speed/11023669#11023669! • http://jakegoulding.com/blog/2012/09/24/a-refactoring-example-if-else-on-strings/

Slide 65

Slide 65 text

@luweidewei weilu Questions?