Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Taming Chaotic Specs: RSpec Design Patterns

Taming Chaotic Specs: RSpec Design Patterns

Presented at RailsRemoteConf.com 2015

Don’t you hate when testing takes 3x as long because your specs are hard to understand? Or when testing conditional permutation leads to a ton of duplication?

Following a few simple patterns, you can easily take a bloated spec and make it readable, DRY and simple to extend. This talk will step through a few solid patterns to tame the chaos. We will take a bloated sample spec and refactor it to something manageable, readable and concise.

Adam Cuppy (he/him)

November 05, 2015
Tweet

More Decks by Adam Cuppy (he/him)

Other Decks in Programming

Transcript

  1. class User < ActiveRecord::Base has_secure_password validates :firstname, presence: true, length:

    4..20 validates :middlename, length: 4..20, allow_blank: true validates :lastname, presence: true, length: 4..20 validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i } validates :password, presence: true, confirmation: true, length: { minimum: 8 } # ... end
  2. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  3. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  4. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  5. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  6. class User < ActiveRecord::Base has_secure_password validates :firstname, presence: true, length:

    4..20 validates :middlename, length: 4..20, allow_blank: true validates :lastname, presence: true, length: 4..20 validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i } validates :password, presence: true, confirmation: true, length: { minimum: 8 } # ... end
  7. class User < ActiveRecord::Base has_secure_password validates :firstname, presence: true, length:

    4..20 validates :middlename, length: 4..20, allow_blank: true validates :lastname, presence: true, length: 4..20 validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i } validates :password, presence: true, confirmation: true, length: { minimum: 8 } # ... end
  8. class User < ActiveRecord::Base has_secure_password validates :firstname, presence: true, length:

    4..20 validates :middlename, length: 4..20, allow_blank: true validates :lastname, presence: true, length: 4..20 validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i } validates :password, presence: true, confirmation: true, length: { minimum: 8 } # ... end Other validations failing!!
  9. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  10. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  11. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  12. describe User do subject(:user) { described_class.new } context 'with a

    firstname that is over 20 chars' do # ... end context 'with a firstname that is under 4 chars' do # ... end end
  13. describe User do subject(:user) { described_class.new firstname: firstname } context

    'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } # ... end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } # ... end end
  14. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  15. # spec/models/user_spec.rb describe User do it "should be invalid" do

    user = User.new(firstname: "really really ... super long firstname") expect(user.valid?).to_not eq true user = User.new(firstname: "sht") expect(user.valid?).to_not eq true end end
  16. describe User do subject(:user) { described_class.new firstname: firstname } context

    'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } # ... end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } # ... end end
  17. describe User do subject(:user) { described_class.new firstname: firstname } context

    'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end Fix false positive ???
  18. describe User do subject(:user) { described_class.new firstname: firstname } context

    'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  19. describe User do subject(:user) { described_class.new firstname: firstname } context

    'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  20. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  21. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  22. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end ??
  23. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  24. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end ?? ??
  25. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  26. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end Fix false positive ???
  27. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  28. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } specify { expect(user).to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } specify { expect(user).to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } specify { expect(user).to be_invalid } end end
  29. describe User do subject(:user) { described_class.new firstname: firstname } let(:firstname)

    { 'Adam' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end context 'with a firstname that is under 4 chars' do let(:firstname) { 'srt' } it { is_expected.to be_invalid } end end
  30. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } let(:email) { '[email protected]' } let(:password) { 'Passw0rd!' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end # ... end
  31. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } let(:email) { '[email protected]' } let(:password) { 'Passw0rd!' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end # ... end #1: Build the subject
  32. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } let(:email) { '[email protected]' } let(:password) { 'Passw0rd!' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end # ... end #1: Build the subject #2: Set the config values
  33. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } let(:email) { '[email protected]' } let(:password) { 'Passw0rd!' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end # ... end #3 Assert a Valid state
  34. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } let(:email) { '[email protected]' } let(:password) { 'Passw0rd!' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end # ... end #4 Mutate
  35. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } let(:email) { '[email protected]' } let(:password) { 'Passw0rd!' } it { is_expected.to be_valid } context 'with a firstname that is over 20 chars' do let(:firstname) { 'really really ... super long firstname' } it { is_expected.to be_invalid } end # ... end
  36. describe User do subject(:user) { described_class.new firstname: firstname, middlename: middlename,

    lastname: lastname, email: email, password: password } let(:firstname) { 'Adam' } let(:middlename) { nil } let(:lastname) { 'Cuppy' } # ... end
  37. describe User do # ... describe 'retrieving a fullname' do

    subject(:fullname) { user.fullname } context 'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end context 'when lastname is blank' do let(:lastname) { nil } it { is_expected.to eq firstname } end context 'when first and lastname are blank' do let(:firstname) { nil } let(:lastname) { nil } it { is_expected.to be_blank } end context 'when there are all three names' do let(:middlename) { 'Jimmy' } it { is_expected.to eq "#{firstname} #{middlename} #{lastname}" } end end # ... end
  38. describe User do # ... describe 'retrieving a fullname' do

    subject(:fullname) { user.fullname } context 'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end context 'when lastname is blank' do let(:lastname) { nil } it { is_expected.to eq firstname } end context 'when first and lastname are blank' do let(:firstname) { nil } let(:lastname) { nil } it { is_expected.to be_blank } end context 'when there are all three names' do let(:middlename) { 'Jimmy' } it { is_expected.to eq "#{firstname} #{middlename} #{lastname}" } end end # ... end
  39. describe User do # ... describe 'retrieving a fullname' do

    subject(:fullname) { user.fullname } context 'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end context 'when lastname is blank' do let(:lastname) { nil } it { is_expected.to eq firstname } end context 'when first and lastname are blank' do let(:firstname) { nil } let(:lastname) { nil } it { is_expected.to be_blank } end context 'when there are all three names' do let(:middlename) { 'Jimmy' } it { is_expected.to eq "#{firstname} #{middlename} #{lastname}" } end end # ... end
  40. describe User do # ... describe 'retrieving a fullname' do

    subject(:fullname) { user.fullname } context 'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end context 'when lastname is blank' do let(:lastname) { nil } it { is_expected.to eq firstname } end context 'when first and lastname are blank' do let(:firstname) { nil } let(:lastname) { nil } it { is_expected.to be_blank } end context 'when there are all three names' do let(:middlename) { 'Jimmy' } it { is_expected.to eq "#{firstname} #{middlename} #{lastname}" } end end # ... end
  41. describe User do # ... describe 'retrieving a fullname' do

    subject(:fullname) { user.fullname } context 'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end context 'when lastname is blank' do let(:lastname) { nil } it { is_expected.to eq firstname } end context 'when first and lastname are blank' do let(:firstname) { nil } let(:lastname) { nil } it { is_expected.to be_blank } end context 'when there are all three names' do let(:middlename) { 'Jimmy' } it { is_expected.to eq "#{firstname} #{middlename} #{lastname}" } end end # ... end Anything missing ??
  42. describe User do # ... describe 'retrieving a fullname' do

    subject(:fullname) { user.fullname } context 'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end context 'when lastname is blank' do let(:lastname) { nil } it { is_expected.to eq firstname } end context 'when first and lastname are blank' do let(:firstname) { nil } let(:lastname) { nil } it { is_expected.to be_blank } end context 'when there are all three names' do let(:middlename) { 'Jimmy' } it { is_expected.to eq "#{firstname} #{middlename} #{lastname}" } end end # ... end
  43. describe 'retrieving a fullname' do subject(:fullname) { user.fullname } context

    'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end #... end
  44. describe 'retrieving a fullname' do subject(:fullname) { user.fullname } context

    'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end #... end
  45. describe 'retrieving a fullname' do subject(:fullname) { user.fullname } context

    'when firstname is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end #... end
  46. describe 'retrieving a fullname' do # context 'when firstname is

    blank' do # let(:firstname) { nil } # it { is_expected.to eq lastname } # end { [nil, 'James', 'Johnson'] => 'James Johnson', }.each do |name_set, output| # ... end end
  47. describe 'retrieving a fullname' do # ... { [nil, 'James',

    'Johnson'] => 'James Johnson', [nil, 'James', nil] => 'James', ['Jimmy', 'James', nil] => 'Jimmy James', ['Jimmy', 'James', 'Johnson'] => 'Jimmy James Johnson' }.each do |name_set, output| # ... end end Anything missing ??
  48. describe 'retrieving a fullname' do # ... { [nil, 'James',

    'Johnson'] => 'James Johnson', [nil, 'James', nil] => 'James', ['Jimmy', 'James', nil] => 'Jimmy James', ['Jimmy', 'James', 'Johnson'] => 'Jimmy James Johnson' ['Jimmy', nil, nil] => 'Jimmy', [nil, nil, 'Johnson'] => 'Johnson', ['Jimmy', nil, 'Johnson'] => 'Jimmy Johnson' }.each do |name_set, output| # ... end end
  49. describe 'retrieving a fullname' do shared_examples_for 'a fullname' do |(first,

    middle, last), output| subject(:fullname) { user.fullname } let(:firstname) { first } let(:middlename) { middle } let(:lastname) { last } it { is_expected.to eq output } end { [nil, 'James', 'Johnson'] => 'James Johnson', [nil, 'James', nil] => 'James', ['Jimmy', 'James', nil] => 'Jimmy James', ['Jimmy', 'James', 'Johnson'] => 'Jimmy James Johnson' ['Jimmy', nil, nil] => 'Jimmy', [nil, nil, 'Johnson'] => 'Johnson', ['Jimmy', nil, 'Johnson'] => 'Jimmy Johnson' }.each do |name_set, output| it_behaves_like 'a fullname', name_set, output end end
  50. describe 'retrieving a fullname' do shared_examples_for 'a fullname' do |(first,

    middle, last), output| subject(:fullname) { user.fullname } let(:firstname) { first } let(:middlename) { middle } let(:lastname) { last } it { is_expected.to eq output } end { [nil, 'James', 'Johnson'] => 'James Johnson', [nil, 'James', nil] => 'James', ['Jimmy', 'James', nil] => 'Jimmy James', ['Jimmy', 'James', 'Johnson'] => 'Jimmy James Johnson' ['Jimmy', nil, nil] => 'Jimmy', [nil, nil, 'Johnson'] => 'Johnson', ['Jimmy', nil, 'Johnson'] => 'Jimmy Johnson' }.each do |name_set, output| it_behaves_like 'a fullname', name_set, output end end
  51. describe 'retrieving a fullname' do shared_examples_for 'a fullname' do |(first,

    middle, last), output| subject(:fullname) { user.fullname } let(:firstname) { first } let(:middlename) { middle } let(:lastname) { last } it { is_expected.to eq output } end { [nil, 'James', 'Johnson'] => 'James Johnson', [nil, 'James', nil] => 'James', ['Jimmy', 'James', nil] => 'Jimmy James', ['Jimmy', 'James', 'Johnson'] => 'Jimmy James Johnson' ['Jimmy', nil, nil] => 'Jimmy', [nil, nil, 'Johnson'] => 'Johnson', ['Jimmy', nil, 'Johnson'] => 'Jimmy Johnson' }.each do |name_set, output| it_behaves_like 'a fullname', name_set, output end end
  52. 1. Backfilling untested legacy code 2. Uncertain expectations require visual

    confirmation 3. Code complexity significantly exceeds current domain knowledge Golden Master Testing
  53. 1. Take a snapshot of an object (to a file)

    2. Verify the snapshot (manually) 3. Compare future versions to the verified “master” Golden Master Testing
  54. # spec/spec_helper.rb require ‘approvals/rspec' # path/to/spec.rb it 'works' do verify

    format: :html do "<html><head></head><body><h1>ZOMG</h1></body></html>" end end # Manually verify snapshots $ cd /path/to/app $ approvals verify https://github.com/kytrinyx/approvals
  55. # spec/spec_helper.rb require ‘approvals/rspec' # path/to/spec.rb it 'works' do verify

    format: :html do "<html><head></head><body><h1>ZOMG</h1></body></html>" end end # Manually verify snapshots $ cd /path/to/app $ approvals verify https://github.com/kytrinyx/approvals
  56. # spec/spec_helper.rb require ‘approvals/rspec' # path/to/spec.rb it 'works' do verify

    format: :html do "<html><head></head><body><h1>ZOMG</h1></body></html>" end end # Manually verify snapshots $ cd /path/to/app $ approvals verify https://github.com/kytrinyx/approvals
  57. let

  58. let(:fullname) { 'Adam Cuppy' } specify { expect(fullnmae).to eq 'Adam

    Cuppy' } # => NameError: undefined local variable or method `fullnmae'
  59. describe 'secure zones' do context 'as a guest user' do

    let(:user) { User.new } # ... end context 'as an admin user' do let(:user) { User.new admin: true } # ... end end
  60. require 'rspec/expectations' RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual

    % expected == 0 end end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } end https://www.relishapp.com/rspec/rspec-expectations/v/3-3/docs/custom-matchers/define-matcher
  61. require 'rspec/expectations' RSpec::Matchers.define :be_a_multiple_of do |expected| match do |actual| actual

    % expected == 0 end end RSpec.describe 9 do it { is_expected.to be_a_multiple_of(3) } end https://www.relishapp.com/rspec/rspec-expectations/v/3-3/docs/custom-matchers/define-matcher
  62. describe User do # ... context 'when a new user

    matches an existing user' do let!(:existing_user) { described_class.create! firstname: firstname, middlename: middlename lastname: lastname, email: email, password: password } let(:new_user) { described_class.new firstname: firstname, middlename: middlename lastname: lastname, email: email, password: password } it { expect(new_user).to be_invalid } end # ... end WHat’s invalid ??
  63. describe User do # ... context 'when a new user

    matches an existing user' do let!(:existing_user) { described_class.create! firstname: firstname, middlename: middlename lastname: lastname, email: email, password: password } let(:new_user) { described_class.new firstname: firstname, middlename: middlename lastname: lastname, email: email, password: password } it { expect(new_user).to be_invalid } end # ... end
  64. describe User do subject(:user) { ... } # ... describe

    'validations' do it { is_expected.to validate_uniqueness_of(:email) } end end https://github.com/thoughtbot/shoulda-matchers
  65. describe User do subject(:user) { ... } # ... describe

    'validations' do it { is_expected.to validate_uniqueness_of(:email) } end end https://github.com/thoughtbot/shoulda-matchers
  66. describe User do subject(:user) { ... } # ... describe

    'validations' do it { is_expected.to validate_uniqueness_of(:email) } end end https://github.com/thoughtbot/shoulda-matchers
  67. describe User do subject(:user) { FactoryGirl.build :user } # ...

    context 'validations' do it { is_expected.to validate_uniqueness_of(:email) } end end
  68. describe User do subject(:user) { FactoryGirl.build :user } # ...

    context 'validations' do it { is_expected.to validate_uniqueness_of(:email) } end end