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

#RailsPacific - [NOTES] Taming Chaotic Specs - ...

#RailsPacific - [NOTES] Taming Chaotic Specs - RSpec Design Patterns

-- PRESENTER NOTES --

Don’t you hate when testing takes 3x as long because your specs are hard to understand? Following a few simple patterns, you can easily take a bloated spec and make it DRY and simple to extend. We will take a bloated sample spec and refactor it to something manageable, readable and concise.

Adam Cuppy (he/him)

May 20, 2016
Tweet

More Decks by Adam Cuppy (he/him)

Other Decks in Programming

Transcript

  1. http://rspec.info/ * Started in 2005 as an experiment by Steven

    Baker * Behavior Driven Development testing framework for Ruby * Declarative DSL (describe, context, it/specify, etc…) * Test framework for many ruby libraries/gems
  2. Problem * Our test suite becomes a second class citizen

    * DSL is thick and the learning curve can be tough * “BUT WE MUST HAVE TESTS!!” So, we deal. * Tests are hard to understand, expand, refactor and confusing
  3. 1. Communicate expectations 2. Encourage consistency 3. Reduce the mental

    load Design Patterns… * Expectations: Ruby is a communicative language * Consistency: 2+ people = communication * Mental load: you don’t to spend cycles digesting what’s being communicated
  4. 1. Start with a “valid” object 2. Make one specific

    change 3. Assert the “valid” object is now “invalid” M.V.O. Workflow…
  5. # app/models/user.rb 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
  6. # 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
  7. # 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
  8. # 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 False positive!!
  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 Mutating the User object and assessing if it’s valid, WHICH IT ISN’T, so what’s wrong?
  10. # app/models/user.rb 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
  11. # app/models/user.rb 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 This validation IS failing, but…
  12. # app/models/user.rb 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!! So are these…our test is passing, but it’s not actually passing for the right reasons.
  13. # 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 COMMUNICATION
  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 Conflicting communication: 1) be valid; and 2) to be true PROBLEM: We didn’t start with a valid object
  15. describe User do # ... end RSpec#subject * default RSpec

    behavior is to initialize the described class (User) >> “User.new” * communicate the change by placing it at the top of the file (overriding the default) * NAME the subject. This communicates what it is. Don’t just leave “subject”.
  16. describe User do subject(:user) { described_class.new } # ... end

    RSpec#subject * default RSpec behavior is to initialize the described class (User) >> “User.new” * communicate the change by placing it at the top of the file (overriding the default) * NAME the subject. This communicates what it is. Don’t just leave “subject”.
  17. describe User do subject(:user) { described_class.new } # ... end

    RSpec#subject * default RSpec behavior is to initialize the described class (User) >> “User.new” * communicate the change by placing it at the top of the file (overriding the default) * NAME the subject. This communicates what it is. Don’t just leave “subject”.
  18. describe User do subject(:user) { described_class.new } # ... end

    RSpec#subject * default RSpec behavior is to initialize the described class (User) >> “User.new” * communicate the change by placing it at the top of the file (overriding the default) * NAME the subject. This communicates what it is. Don’t just leave “subject”.
  19. # 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 Two different assertions: long name and short name, but in the same expectation block.
  20. # 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 Two different assertions: long name and short name, but in the same expectation block.
  21. describe User do subject(:user) { described_class.new } # ... end

    Isolate each expectation in their own context (which loosely interpreted means “in this situation”)
  22. 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 Isolate each expectation in their own context (which loosely interpreted means “in this situation”)
  23. 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 Too Long Isolate each expectation in their own context (which loosely interpreted means “in this situation”)
  24. 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 Too Short Isolate each expectation in their own context (which loosely interpreted means “in this situation”)
  25. 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 Isolate each expectation in their own context (which loosely interpreted means “in this situation”)
  26. 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 Add the mutated value with let. If you’re not familiar with let or have used @instance vars in the past, I’ll explain the value at the end. Basics: let’s are lazy loaded (and cached), but allow you to override a parent example block
  27. 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 Add the mutated value with let. If you’re not familiar with let or have used @instance vars in the past, I’ll explain the value at the end. Basics: let’s are lazy loaded (and cached), but allow you to override a parent example block
  28. # 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 Two different assertions: long name and short name, but in the same expectation block.
  29. # 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
  30. # 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
  31. (true, not true?) expect(user).to_not be_valid RSpec has support for predicate

    methods: any predicate method on an object (with “?”) is automatically transposed in RSpec to a matcher.
  32. 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
  33. 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
  34. 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 ???
  35. 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 Still haven’t validated that it’s good, at first.
  36. 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 This is where the Minimum Valid comes in…
  37. 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 Added the default/valid values for the object and assert that before we begin mutating values, that the parent object is in a valid state.
  38. 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 ??
  39. 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
  40. 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 ?? ??
  41. 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
  42. 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 ??? When the validations run and it discovers the failed validations, it will catch those first. It’s a “linter” for our code.
  43. 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
  44. 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 Love it or hate it, we can clean this up, as well
  45. 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
  46. 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 When we build out our MVO, it might look something like this…
  47. 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 1. build the MINIMUM valid object
  48. 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 #2: Set the config values 1. Include all the configurable values in their min required state (ex: middlename) to communicate what’s part of a valid object. Long spec? This helps resolve what can change.
  49. 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
  50. 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 Mutate the config value with a greater degree of CONFIDENCE that what’s changes is related to a potential failure
  51. 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
  52. 1. Define sets of data 2. Define the output of

    each set 3. Assert the method creates the output from the data (input/output) P.T. Workflow…
  53. describe User do # ... describe ‘#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 Following an MVO, our spec may end up a little like this…
  54. describe User do # ... describe ‘#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
  55. describe User do # ... describe ‘#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
  56. describe User do # ... describe ‘#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
  57. describe User do # ... describe ‘#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 ??
  58. describe User do # ... describe ‘#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
  59. describe ‘#fullname' do subject(:fullname) { user.fullname } context 'when firstname

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

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

    is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end #... end The test doesn’t tell us how the two are related << COMMUNICATION BROKEN!!
  62. describe ‘#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 Firstname Introduce a PERMUTATION TABLE * We see the player in the game (first, mid, last) * And, the result of those players (mid, last)
  63. describe ‘#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 middlename Introduce a PERMUTATION TABLE * We see the player in the game (first, mid, last) * And, the result of those players (mid, last)
  64. describe ‘#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 Lastname Introduce a PERMUTATION TABLE * We see the player in the game (first, mid, last) * And, the result of those players (mid, last)
  65. describe ‘#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 Fullname Introduce a PERMUTATION TABLE * We see the player in the game (first, mid, last) * And, the result of those players (mid, last)
  66. describe ‘#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 I can visually see what’s missing now…
  67. describe ‘#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 ?? I can visually see what’s missing now…
  68. describe ‘#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 so, I can fill in the gaps
  69. describe ‘#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
  70. describe ‘#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 * Move our common code to a shared example that unpacks the segments and compares to the output * Gotcha: DON’T back too much logic into this. We don’t want to test the test
  71. describe ‘#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
  72. describe ‘#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
  73. 1. Backfilling untested legacy code 2. Uncertain expectations require visual

    confirmation 3. Code complexity significantly exceeds current domain knowledge Golden Master Testing
  74. require 'rails_helper' RSpec.describe User, type: :model do pending "add some

    examples to (or delete) #{__FILE__}" end This sucks to find
  75. 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 Workflow… 3.
  76. # 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
  77. # 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 * Captures an output (could be a method’s output) * Write it to a file (appended “received”)
  78. # 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 * steps through “received” snapshots * allow you to manually verify the output (appends with “approved”) * saves it for later comparison
  79. Best Practices Better Good, but not Required Other ways of

    doing things, and why it might be helpfuL…ideas
  80. Best Practices Better Good, but not Required Other ways of

    doing things, and why it might be helpfuL…ideas
  81. let(:fullname) { 'Adam Cuppy' } specify { expect(fullnmae).to eq 'Adam

    Cuppy' } # => NameError: undefined local variable or method `fullnmae'
  82. describe 'validations' do subject(:user) { User.new email: '[email protected]' } it

    { is_expected.to be_valid } context 'when an email is used' do let(:user_with_duplicate_email) do User.new email: user.email end it { expect(user_with_duplicate_email).to be_invalid } end end
  83. describe 'validations' do subject(:user) { User.new email: '[email protected]' } it

    { is_expected.to be_valid } context 'when an email is used' do let(:user_with_duplicate_email) do User.new email: user.email end it { expect(user_with_duplicate_email).to be_invalid } end end
  84. describe 'validations' do subject(:user) { User.new email: '[email protected]' } it

    { is_expected.to be_valid } context 'when an email is used' do let(:user_with_duplicate_email) do User.new email: user.email end it { expect(user_with_duplicate_email).to be_invalid } end end
  85. describe 'validations' do subject(:user) { User.new email: '[email protected]' } it

    { is_expected.to be_valid } context 'when an email is used' do let(:user_with_duplicate_email) do User.new email: user.email end it { expect(user_with_duplicate_email).to be_invalid } end end
  86. 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
  87. 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
  88. describe User do subject(:user) { FactoryGirl.build :user } # ...

    end * Allow your to build Minimum Valid Objects