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

#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.

Fff206665889a0f5baeedf5b75483edd?s=128

Adam Cuppy

May 20, 2016
Tweet

Transcript

  1. www.codingzeal.com @adamcuppy Adam Cuppy - CodingZeal.com - Custom Web &

    Mobile Apps - Consultants for hire info@codingzeal.com
  2. github.com/acuppy twitter.com/adamcuppy - @AdamCuppy

  3. speakerdeck.com/ acuppy

  4. 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
  5. 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
  6. Taming Chaotic Specs RSpec Design Patterns

  7. Not what to test, But…

  8. Patterns to follow when structuring the test(s)

  9. 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
  10. Minimum Valid Object

  11. 1. Start with a “valid” object 2. Make one specific

    change 3. Assert the “valid” object is now “invalid” M.V.O. Workflow…
  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
  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
  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 False positive!!
  16. # 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?
  17. # 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
  18. # 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…
  19. # 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.
  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 COMMUNICATION
  21. # 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
  22. 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”.
  23. 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”.
  24. 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”.
  25. 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”.
  26. # 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.
  27. # 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.
  28. describe User do subject(:user) { described_class.new } # ... end

    Isolate each expectation in their own context (which loosely interpreted means “in this situation”)
  29. 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”)
  30. 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”)
  31. 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”)
  32. 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”)
  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' } # ... 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
  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' } # ... 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
  35. # 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.
  36. # 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
  37. # 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
  38. expect(user.valid?).to_not eq true (true, not true?)

  39. expect(user.valid?).to_not eq true (true, not true?)

  40. RSpec Predicate Magic Methods

  41. user.valid?

  42. user.valid? expect(user).to_not be_valid

  43. user.valid? expect(user).to_not be_valid

  44. user.riding_a_monkey? expect(user).to_not be_riding_a_monkey

  45. (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.
  46. (true?) expect(user).to be_invalid Rails has a method #invalid? which will

    help
  47. (true?) expect(user).to be_invalid

  48. 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
  49. 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
  50. 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 ???
  51. 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.
  52. 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…
  53. 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.
  54. 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 ??
  55. 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
  56. 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 ?? ??
  57. 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
  58. 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.
  59. 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
  60. 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
  61. 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
  62. 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) { 'adam@codingzeal.com' } 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…
  63. 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) { 'adam@codingzeal.com' } 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
  64. 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) { 'adam@codingzeal.com' } 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.
  65. 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) { 'adam@codingzeal.com' } 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
  66. 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) { 'adam@codingzeal.com' } 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
  67. 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) { 'adam@codingzeal.com' } 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
  68. Permutation Tables

  69. 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…
  70. class User < ActiveRecord::Base # ... def fullname [firstname, middlename,

    lastname].compact.join ' ' end end
  71. class User < ActiveRecord::Base # ... def fullname [firstname, middlename,

    lastname].compact.join ' ' end end
  72. class User < ActiveRecord::Base # ... def fullname [firstname, middlename,

    lastname].compact.join ' ' end end
  73. class User < ActiveRecord::Base # ... def fullname [firstname, middlename,

    lastname].compact.join ' ' end end
  74. 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…
  75. 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
  76. describe '#fullname' do subject(:fullname) { user.fullname } # ... end

  77. 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
  78. 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
  79. 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 ??
  80. 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
  81. describe ‘#fullname' do subject(:fullname) { user.fullname } context 'when firstname

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

    is blank' do let(:firstname) { nil } it { is_expected.to eq lastname } end #... end
  83. 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!!
  84. 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)
  85. 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)
  86. 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)
  87. 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)
  88. 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…
  89. 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…
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. Golden Master

  96. 1. Backfilling untested legacy code 2. Uncertain expectations require visual

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

    examples to (or delete) #{__FILE__}" end This sucks to find
  98. 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.
  99. https://github.com/kytrinyx/approvals

  100. # 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
  101. # 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”)
  102. # 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
  103. Best Practices

  104. Best Practices Better

  105. Best Practices Better Good, but not Required

  106. Best Practices Better Good, but not Required Other ways of

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

    doing things, and why it might be helpfuL…ideas
  108. let (not @instance vars)

  109. @fullname = 'Adam Cuppy' specify { expect(@fullnmae).to eq 'Adam Cuppy'

    }
  110. @fullname = 'Adam Cuppy' specify { expect(@fullnmae).to eq 'Adam Cuppy'

    } # => expected: "Adam Cuppy", got: nil ???
  111. @fullname = 'Adam Cuppy' specify { expect(@fullnmae).to eq 'Adam Cuppy'

    }
  112. @fullname = 'Adam Cuppy' specify { expect(@fullnmae).to eq 'Adam Cuppy'

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

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

    Cuppy' } # => NameError: undefined local variable or method `fullnmae'
  115. Descriptive Naming

  116. it { expect(user1).to be_valid } it { expect(user2).to be_invalid }

  117. it { expect(user).to be_valid } it { expect(user_with_duplicate_email).to be_invalid }

  118. describe 'validations' do subject(:user) { User.new email: 'joe@example.com' } 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
  119. describe 'validations' do subject(:user) { User.new email: 'joe@example.com' } 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
  120. describe 'validations' do subject(:user) { User.new email: 'joe@example.com' } 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
  121. describe 'validations' do subject(:user) { User.new email: 'joe@example.com' } 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
  122. Extract Common Expectations

  123. Custom Matchers

  124. 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
  125. 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
  126. Factories over Fixtures

  127. https://github.com/thoughtbot/factory_girl

  128. describe User do subject(:user) { User.new } # ... end

  129. describe User do subject(:user) { FactoryGirl.build :user } # ...

    end
  130. describe User do subject(:user) { FactoryGirl.build :user } # ...

    end * Allow your to build Minimum Valid Objects
  131. FactoryGirl.lint! * Validates your ActiveRecord objects are valid

  132. https://github.com/thoughtbot/factory_girl * Downside: hides a lot of model construction, so

    I find that it starts small and grows fast (hiding a lot)
  133. Readables

  134. http://betterspecs.org/

  135. Randy Coulman - http://randycoulman.com/blog/categories/getting-testy/

  136. Randy Coulman - http://randycoulman.com/blog/categories/getting-testy/

  137. Sandi Metz - http://www.poodr.com

  138. https://codingzeal.com/

  139. www.codingzeal.com @adamcuppy Adam Cuppy