Testing 201

Testing 201

A9704266587836f7e784235e5073b93e?s=128

Joseph Mastey

April 15, 2019
Tweet

Transcript

  1. TESTING 201 OR: GREAT EXPECTATIONS JOE MASTEY, APRIL 2019

  2. EVERYONE IS DOING THEIR BEST

  3. HEURISTICS, NOT RULES

  4. THIS STUFF SHOULDN’T SLOW YOU DOWN

  5. PROLOGUE: THE TROUBLE WITH A SIMPLE TEST

  6. class CreateShipmentHistoryReport attr_accessor :user, :shipments def initialize(user) @user = user

    @shipments = user.shipments end def process shipments.sort_by(&:created_at) data = shipments.map do |shipment| shipment.to_json.values + [ shipment.user.name, shipment.product.name ] end file = CSV.open(tmp_filename) do |csv| csv << shipments.first.to_json.keys + ['user', 'product'] data.map do |row| csv << row end end persist_csv_to_s3(file) notify_user(user.email) file end end
  7. class CreateShipmentHistoryReport attr_accessor :user, :shipments def initialize(user) @user = user

    @shipments = user.shipments end
  8. def process shipments.sort_by(&:created_at) data = shipments.map do |shipment| shipment.to_json.values +

    [ shipment.user.name, shipment.product.name ] end # continued below...
  9. # ... process continued file = CSV.open(tmp_filename) do |csv| csv

    << shipments.first.to_json.keys + ['user', 'product'] data.map do |row| csv << row end end persist_csv_to_s3(file) notify_user(user.email) file end
  10. class CreateShipmentHistoryReport attr_accessor :user, :shipments def initialize(user) @user = user

    @shipments = user.shipments end def process shipments.order('created_at desc') data = shipments.map do |shipment| shipment.to_json.values + [ shipment.user.name, shipment.product.name ] end file = CSV.open(tmp_filename) do |csv| csv << shipments.first.to_json.keys + ['user', 'product'] data.map do |row| csv << row end end persist_csv_to_s3(file) notify_user(user.email) file end end
  11. describe CreateShipmentHistoryReport do describe "#process" do it "generates an array

    of strings" do expect(csv_data.length).to eq(shipments.length) expect(csv_data).to all(be_an(Array)) end it "generates an output record for each input, sorted by date" do expect(csv_data).to eq(output_records) end end end
  12. let(:subject) { described_class.new(user).process } let(:csv_data) { CSV.parse(subject) }

  13. let(:user) { create(:user, :signup_complete) } let(:subscription) { create(:subscription) } before

    do user.subscription << subscription end
  14. let(:product) { create(:product) } let!(:shipment) { create(:shipment, product: product, created_at:

    2.days.ago) } let(:output_records) do [ [shipment.created_on, shipment.completed_on, user.name, product.name] ] end before do expect(user) .to receive(:shipments) .and_return([shipment]) end end
  15. before do expect_any_instance_of(described_class) .to receive(:save_to_s3) .and_return(true) expect_any_instance_of(described_class) .to receive(:email_user) .and_return(true)

    end
  16. let(:product_1) { create(:product) } let(:product_2) { create(:product) } let(:product_3) {

    create(:product) } let(:product_4) { create(:product) }
  17. let!(:shipment_1) { create(:shipment, product: product_1, created_at: 2.days.ago) } let!(:shipment_2) {

    create(:shipment, product: product_2, created_at: 3.days.ago) } let!(:shipment_3) { create(:shipment, product: product_3, created_at: 4.days.ago) } let!(:shipment_4) { create(:shipment, product: product_4, created_at: 5.days.ago) }
  18. let(:shipments) do [ shipment_1, shipment_2, shipment_3, shipment_4 ] end

  19. let(:output_records) do [ [shipment_4.created_on, shipment_4.completed_on, user.name, product_4.name], [shipment_3.created_on, shipment_3.completed_on, user.name,

    product_3.name], [shipment_2.created_on, shipment_2.completed_on, user.name, product_2.name], [shipment_1.created_on, shipment_1.completed_on, user.name, product_1.name], ] end
  20. it "generates an output record for each input, sorted by

    date" do expect(csv_data).to eq(output_records) end
  21. describe CreateShipmentHistoryReport do describe "#process" do let(:user) { create(:user, :signup_complete)

    } let(:subscription) { create(:subscription) } let(:product_1) { create(:product) } let(:product_2) { create(:product) } let(:product_3) { create(:product) } let(:product_4) { create(:product) } let!(:shipment_1) { create(:shipment, product: product_1, created_at: 2.days.ago) } let!(:shipment_2) { create(:shipment, product: product_2, created_at: 3.days.ago) } let!(:shipment_3) { create(:shipment, product: product_3, created_at: 4.days.ago) } let!(:shipment_4) { create(:shipment, product: product_4, created_at: 5.days.ago) } let(:subject) { described_class.new(user).process } let(:csv_data) { CSV.parse(subject) } let(:shipments) do [ shipment_1, shipment_2, shipment_3, shipment_4 ] end let(:output_records) do [ [shipment_4.created_on, shipment_4.completed_on, user.name, product_4.name], [shipment_3.created_on, shipment_3.completed_on, user.name, product_3.name], [shipment_2.created_on, shipment_2.completed_on, user.name, product_2.name], [shipment_1.created_on, shipment_1.completed_on, user.name, product_1.name], ] end before do user.subscription << subscription expect(user) .to receive(:shipments) .and_return(shipments) expect_any_instance_of(described_class) .to receive(:save_to_s3) .and_return(true) expect_any_instance_of(described_class) .to receive(:email_user) .and_return(true) end it "generates an array of strings" do expect(csv_data.length).to eq(shipments.length) expect(csv_data).to all(be_an(Array)) end it "generates an output record for each input, sorted by date" do expect(csv_data).to eq(output_records) end end end
  22. FOCUSING ON THE WRONG THINGS

  23. THE 3 ROLES OF TESTING (IN ORDER)

  24. 1. DESIGN FEEDBACK 2. DOCUMENTATION 3. VERIFICATION

  25. TESTS AS DESIGN FEEDBACK

  26. A NOTE ON TDD

  27. CODE THAT’S DIFFICULT TO TEST IS TRYING TO TELL YOU

    SOMETHING IMPORTANT
  28. IT’S EASIER TO WORK WITH OBJECTS IN SMALL CHUNKS.

  29. let(:csv_data) { CSV.parse(subject) } before do expect_any_instance_of(described_class) .to receive(:save_to_s3) .and_return(true)

    expect_any_instance_of(described_class) .to receive(:email_user) .and_return(true) end
  30. before do expect_any_instance_of(described_class) .to receive(:save_to_s3) .and_return(true) expect_any_instance_of(described_class) .to receive(:email_user) .and_return(true)

    end
  31. def data(shipments) shipments.sort_by!(&:created_at) shipments.map do |shipment| shipment.to_json.values + [ shipment.user.name,

    shipment.product.name ] end end def headers shipments.first.to_json.keys + ['user', 'product'] end
  32. file = CSV.open(tmp_filename) do |csv| csv << headers data(shipments).map {

    |row| csv << row } end
  33. it "generates an output record for each input, sorted by

    date" do subject = described_class.new(user) expect(subject.data).to eq(output_records) end
  34. before do # expect_any_instance_of(described_class) # .to receive(:save_to_s3) # .and_return(true) #

    expect_any_instance_of(described_class) # .to receive(:email_user) # .and_return(true) end
  35. before do expect(user) .to receive(:shipments) .and_return(shipments) end

  36. class CreateShipmentHistoryReport def initialize(user, shipments: user.shipments) @user = user @shipments

    = shipments end end
  37. before do # expect(user) # .to receive(:shipments) # .and_return(shipments) end

    described_class.new(user, shipments)
  38. def data(shipments) shipments.sort_by!(&:created_at) shipments.map do |shipment| shipment.to_json.values + [ shipment.user.name,

    shipment.product.name ] end end def headers shipments.first.to_json.keys + ['user', 'product'] end
  39. def serialize(shipment) { created_on: shipment.created_on, completed_on: shipment.completed_on, user: shipment.user.name, product:

    shipment.product.name, } end
  40. def serialize(shipment) { created_on: shipment.created_on, completed_on: shipment.completed_on, user: shipment.user.name, product:

    shipment.product.name, } end def data(shipments) shipments.sort_by(&:created_at).map do |shipment| serialize(shipment) end end def headers serialize(shipments.first).keys end
  41. describe "#serialize" do it "serializes some shipment and user data"

    do shipment = create_shipment(date: 2.days.ago) report = described_class.new(user, []) result = report.serialize(shipment) expect(result).to eq({ created_on: shipment.created_on, completed_on: shipment.completed_on, user: shipment.user.name, product: shipment.product.name, }) end end
  42. # let(:output_records) do # [ # [shipment_4.created_on, shipment_4.completed_on, # user.name,

    product_4.name], # [shipment_3.created_on, shipment_3.completed_on, # user.name, product_3.name], # [shipment_2.created_on, shipment_2.completed_on, # user.name, product_2.name], # [shipment_1.created_on, shipment_1.completed_on, # user.name, product_1.name], # ] # end
  43. describe "ordering" do it "reorders shipments by their creation date"

    do shipment_1, shipment_2 report = described_class.new(user, [shipment_1, shipment_2]) result = report.data expect(result.map(&:first)).to eq( [shipment_2.created_on, shipment_1.created_on] ) end end
  44. CREATE AS FEW RECORDS AS YOU CAN (AND ONLY RELATED

    ONES)
  45. let(:user) { create(:user, :signup_complete) } let(:subscription) { create(:subscription) } before

    do user.subscription << subscription end
  46. Factory AR records AR queries create(:user) 1 10 create(:meal) 5

    41 create(:full_menu) 94 584 create(:weekly_basket) 104 644 create(:user, :with_order_history) 379 2336
  47. let(:user) do instance_double(User, email: "test@test.com", shipments: shipments) end

  48. let(:user) { User.new(email: "test@test.com", name: "Joe") }

  49. describe "ordering" do it "reorders shipments by their creation date"

    do shipment_1, shipment_2 report = described_class.new(user, [shipment_1, shipment_2]) result = report.data expect(result.map(&:first)).to eq( [shipment_2.created_on, shipment_1.created_on] ) end end
  50. PAY ATTENTION TO SIMILARITY AND DIFFERENCE

  51. let(:product_1) { create(:product) } let(:product_2) { create(:product) } let(:product_3) {

    create(:product) } let(:product_4) { create(:product) }
  52. let!(:shipment_1) { create(:shipment, product: product_1, created_at: 2.days.ago) } let!(:shipment_2) {

    create(:shipment, product: product_2, created_at: 3.days.ago) } let!(:shipment_3) { create(:shipment, product: product_3, created_at: 4.days.ago) } let!(:shipment_4) { create(:shipment, product: product_4, created_at: 5.days.ago) }
  53. let(:shipments) do [ shipment_1, shipment_2, shipment_3, shipment_4 ] end

  54. let(:output_records) do [ [shipment_4.created_on, shipment_4.completed_on, user.name, product_4.name], [shipment_3.created_on, shipment_3.completed_on, user.name,

    product_3.name], [shipment_2.created_on, shipment_2.completed_on, user.name, product_2.name], [shipment_1.created_on, shipment_1.completed_on, user.name, product_1.name], ] end
  55. def create_shipment(date:) product = create(:product) create(:shipment, product: product, created_at: date)

    end
  56. describe "ordering" do it "reorders shipments by their creation date"

    do old = create_shipment(date: 9.days.ago) new = create_shipment(date: 2.days.ago) report = described_class.new(user, [new, old]) result = report.data expect(result.map(&:first)).to eq( [old.created_on, new.created_on] ) end end
  57. # let(:product_1) { create(:product) } # let(:product_2) { create(:product) }

    # let(:product_3) { create(:product) } # let(:product_4) { create(:product) } # let!(:shipment_1) { create(:shipment, product: product_1, # created_at: 2.days.ago) } # let!(:shipment_2) { create(:shipment, product: product_2, # created_at: 3.days.ago) } # let!(:shipment_3) { create(:shipment, product: product_3, # created_at: 4.days.ago) } # let!(:shipment_4) { create(:shipment, product: product_4, # created_at: 5.days.ago) } # # let(:shipments) do # [ # shipment_1, # shipment_2, # shipment_3, # shipment_4 # ] # end # let(:output_records) do # [ # [shipment_4.created_on, shipment_4.completed_on, # user.name, product_4.name], # [shipment_3.created_on, shipment_3.completed_on, # user.name, product_3.name], # [shipment_2.created_on, shipment_2.completed_on, # user.name, product_2.name], # [shipment_1.created_on, shipment_1.completed_on, # user.name, product_1.name], # ] # end
  58. TRY TO REDUCE COMPLEXITY, NOT HIDE IT

  59. def create_shipment(date:) product = create(:product) create(:shipment, product: product, created_at: date)

    end
  60. it "generates an output record for each input, sorted by

    date" do expect(csv_data).to eq(output_records) end
  61. DON’T DO TOO MUCH WORK

  62. def process def serialize(shipment) def data(shipments) def headers def save_to_s3(file)

    def notify_user(email)
  63. None
  64. TESTS AS DOCUMENTATION

  65. WRITE TESTS FOR HUMANS, NOT FOR THE COMPILER

  66. it "generates an output record for each input, sorted by

    date" do expect(csv_data).to eq(output_records) end
  67. describe "ordering" do it "reorders shipments by their creation date"

    do old = create_shipment(date: 9.days.ago) new = create_shipment(date: 2.days.ago) report = described_class.new(user, [new, old]) result = report.data expect(result.map(&:first)).to eq( [old.created_on, new.created_on] ) end end
  68. it "serializes data as an array" do users = create_list(:user,

    3) response = subject.create(User.all).process response = response[0][4] expect(response).to eq(users.first.name) end
  69. it "serializes data as an array" do users = create_list(:user,

    3) serialized_users = subject.create(User.all).to_hashes response = serialized_users.first.full_name expect(response).to eq(users.first.name) end
  70. TRY TO STICK TO A GIVEN/ WHEN/THEN STRUCTURE

  71. describe "ordering" do it "reorders shipments by their creation date"

    do old = create_shipment(date: 9.days.ago) new = create_shipment(date: 2.days.ago) report = described_class.new(user, [new, old]) result = report.data expect(result.map(&:first)).to eq( [old.created_on, new.created_on] ) end end
  72. describe "ordering" do it "reorders shipments by their creation date"

    do # Given old = create_shipment(date: 9.days.ago) new = create_shipment(date: 2.days.ago) # When report = described_class.new(user, [new, old]) result = report.data # Then expect(result.map(&:first)).to eq( [old.created_on, new.created_on] ) end end
  73. it "reorders shipments by their creation date" do old =

    create_shipment(date: 9.days.ago) new = create_shipment(date: 2.days.ago) report = described_class.new(user, [new, old]) expect(report.data.map(&:first)).to eq([old, new]) end
  74. before do expect_any_instance_of(described_class) .to receive(:save_to_s3) .and_return(true) expect_any_instance_of(described_class) .to receive(:email_user) .and_return(true)

    end
  75. USING G/W/T EMPHASIZES SAMENESS AND DIFFERENCE.

  76. it "returns order units summed, divided by units per shipper"

    do create_store_order(count_units: 4) create_store_order(count_units: 8) subject = described_class.new(store_orders: store_orders) total_shippers = subject.total_shipper_count expect(total_shippers).to eq(3) end
  77. it "returns a ceiling rounded value" do create_store_order(count_units: 7) subject

    = described_class.new(store_orders: store_orders) total_shippers = subject.total_shipper_count expect(total_shippers).to eq(2) end
  78. let!(:shipment_1) { create(:shipment, product: product_1, created_at: 2.days.ago) } let!(:shipment_2) {

    create(:shipment, product: product_2, created_at: 3.days.ago) } let!(:shipment_3) { create(:shipment, product: product_3, created_at: 4.days.ago) } let!(:shipment_4) { create(:shipment, product: product_4, created_at: 5.days.ago) }
  79. TESTS DON’T NEED TO BE (TOO) DRY

  80. it "returns order units summed, divided by units per shipper"

    do create_store_order(count_units: 4) create_store_order(count_units: 8) subject = described_class.new(store_orders: store_orders) total_shippers = subject.total_shipper_count expect(total_shippers).to eq(3) end it "returns a ceiling rounded value" do create_store_order(count_units: 7) subject = described_class.new(store_orders: store_orders) total_shippers = subject.total_shipper_count expect(total_shippers).to eq(2) end
  81. describe "#total_shipper_count" do subject { described_class.new(store_orders: StoreOrder.all) } context "even

    division" do let!(:order1) { create_store_order(count_units: 4) } let!(:order2) { create_store_order(count_units: 8) } let(:expected_total) { 3 } it "returns order units summed, divided by units per shipper" do expect(subject.total_shipper_count).to eq(expected_total) end end end
  82. context "rounding" do let!(:order) { create_store_order(count_units: 7) } let(:expected_total) {

    2 } it "returns order units summed, divided by units per shipper" do expect(subject.total_shipper_count).to eq(expected_total) end end
  83. subject(:service) { described_class.new } it "sends email" do expect(service).to receive(:notify_user)

    end after do service.process end
  84. THE FEELING OF NEEDING TO DRY YOUR TESTS IS ACTUALLY

    DESIGN FEEDBACK
  85. SAY WHAT YOU MEAN, LITERALLY

  86. def user_summary(user) "(#{user.id}) #{user.full_name}, #{user.role}" end it "returns a user

    summary for printing" do user = User.new(id: 5, full_name: "Dave G", role: :admin) expected = "(#{user.id}) #{user.full_name}, #{user.role}" summary = user_summary(user) expect(summary).to eq(expected) end
  87. it "returns a user summary for printing" do user =

    User.new(id: 5, full_name: "Dave G", role: :admin) summary = user_summary(user) expect(summary).to eq("(5) Dave G, admin") end
  88. LEVERAGE YOUR TOOLS TO HELP GUIDE READERS

  89. describe "#process" do it "return value" end describe "#process" do

    it "returns the number of records correctly persisted" end
  90. describe CreateShipmentHistoryReport do describe "#sorted_shipments" do it "returns an empty

    array when there are no shipments" end end be rspec ./spec/services/create_shipment_history_report_spec.rb —format=‘documentation’ # CreateShipmentHistoryReport#sorted_shipments returns an empty array when there are no shipments
  91. it “picks randomly, but correctly” do double1 = double double2

    = double result = [double1, double2].sample expect(result).to eq(double1) end # expected: #<Double (anonymous)> # got: #<Double (anonymous)>
  92. it “picks randomly, but correctly” do double1 = double("first record")

    double2 = double("second record”) result = [double1, double2].sample expect(result).to eq(double1) end # expected: #<Double "first record"> # got: #<Double "second record">
  93. it "checks result size" do arr = [1,2,3] response =

    arr.length expect(response > 3).to be_true end # expected true # got false
  94. it "checks result size" do arr = [1,2,3] response =

    arr.length expect(response).to be > 3 end # expected: > 3 # got: 3
  95. it "checks validity of a record" do thing = double(valid?:

    true) expect(thing.valid?).to be_false end # expected: false # got: true
  96. it "checks validity of a record" do thing = double("order",

    valid?: true) expect(thing).not_to be_valid end # expected `#<Double "order">.valid?` to return false, got true
  97. None
  98. TESTS AS VERIFICATION

  99. TEST WHAT’S IMPORTANT, RISKY, OR WHAT’S CHEAP

  100. YOU CAN’T HAVE 100% COVERAGE

  101. DON’T TEST WHAT YOU DON’T OWN

  102. validate :status, presence: true it { should validate_presence_of(:status) }

  103. validate :failure_message, presence: true, if: :failed? it "sometimes requires failure_message"

    do job = JobLog.new(status: :failed) expect(job).to validate_presence_if(:failure_message) job = JobLog.new(status: :completed) expect(job).not_to validate_presence_if(:failure_message) end
  104. DON’T BE AFRAID TO DELETE YOUR TESTS

  105. it "generates an array of strings” do expect(csv_data.length).to eq(shipments.length) expect(csv_data).to

    all(be_an(Array)) end
  106. BEWARE OF OVER-MOCKING

  107. def user_summary(user) "(#{user.id}) #{user.full_name}, #{user.role}" end it "returns a user

    summary for printing, bad" do user = double(id: 5, full_name: "Dave G", role: :admin) response = user_summary(user) expect(response).to eq("(5) Dave G, admin") end
  108. it "returns a user summary for printing, better" do user

    = instance_double(User, id: 5, full_name: "Dave G", role: :admin) response = user_summary(user) expect(response).to eq("(5) Dave G, admin") end
  109. it "returns a user summary for printing, better" do user

    = User.new(id: 5, full_name: "Dave G", role: :admin) expect(user_summary(user)).to eq("(5) Dave G, admin") end
  110. None
  111. IN SUMMARY!

  112. 2. ACTUALLY, JUST DELETE THEM 3. TINY PIGS ARE WAY

    CUTER THAN CATS 1. KEEP YOUR TESTS MOIST
  113. 2. WRITE TESTS FOR HUMANS 3. TINY PIGS ARE WAY

    CUTER THAN CATS 1. USE TESTS TO DRIVE DESIGN
  114. THANKS!