Slide 1

Slide 1 text

TESTING 201 OR: GREAT EXPECTATIONS JOE MASTEY, AUGUST 2020

Slide 2

Slide 2 text

LOTS OF RSPEC, FACTORY BOT, AND RAILS

Slide 3

Slide 3 text

EVERYONE IS DOING THEIR BEST

Slide 4

Slide 4 text

HEURISTICS, NOT RULES

Slide 5

Slide 5 text

PROLOGUE: THE TROUBLE WITH A SIMPLE TEST

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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 "matches the expected output" do expect(csv_data).to eq(output_records) end end end

Slide 12

Slide 12 text

let(:subject) { described_class.new(user).process } let(:csv_data) { CSV.parse(subject) }

Slide 13

Slide 13 text

let(:user) { create(:user, :signup_complete) } let(:subscription) { create(:subscription) } before do user.subscriptions << subscription expect(user) .to receive(:shipments) .and_return(shipments) end

Slide 14

Slide 14 text

let(:shipments) do [ shipment_1, shipment_2, shipment_3, shipment_4 ] end

Slide 15

Slide 15 text

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) }

Slide 16

Slide 16 text

let(:product_1) { create(:product) } let(:product_2) { create(:product) } let(:product_3) { create(:product) } let(:product_4) { create(:product) }

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

it "matches the expected output" do expect(csv_data).to eq(output_records) end

Slide 20

Slide 20 text

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.subscriptions << 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 "matches the expected output" do expect(csv_data).to eq(output_records) end end end

Slide 21

Slide 21 text

FOCUSING ON THE WRONG THINGS

Slide 22

Slide 22 text

THE 3 ROLES OF TESTING (IN ORDER)

Slide 23

Slide 23 text

1. DESIGN FEEDBACK 2. DOCUMENTATION 3. VERIFICATION

Slide 24

Slide 24 text

TESTS AS DESIGN FEEDBACK

Slide 25

Slide 25 text

A NOTE ON TDD

Slide 26

Slide 26 text

CODE THAT’S DIFFICULT TO TEST IS TRYING TO TELL YOU SOMETHING IMPORTANT

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

def data(shipments: @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

Slide 30

Slide 30 text

file = CSV.open(tmp_filename) do |csv| csv << headers data(shipments).map { |row| csv << row } end

Slide 31

Slide 31 text

it "matches the expected output" do subject = described_class.new(user) expect(subject.data).to eq(output_records) end

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

before do # expect(user) # .to receive(:shipments) # .and_return(shipments) end described_class.new(user, shipments)

Slide 36

Slide 36 text

it "matches the expected output" do subject = described_class.new(user, shipments) expect(subject.data).to eq(output_records) end

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

def data(shipments) shipments.sort_by!(&:created_at) shipments.map { |shipment| serialize(shipment) } end def headers serialize(shipments.first).keys end

Slide 40

Slide 40 text

describe "#serialize" do it "serializes some shipment and user data" do shipment = create(:shipment) 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

Slide 41

Slide 41 text

# 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

Slide 42

Slide 42 text

describe "ordering" do it "reorders shipments by their creation date" do 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

Slide 43

Slide 43 text

CREATE AS FEW RECORDS AS YOU CAN (AND ONLY RELATED ONES)

Slide 44

Slide 44 text

let(:user) { create(:user, :signup_complete) } let(:subscription) { create(:subscription) } before do user.subscriptions << subscription end

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

let(:user) do instance_double(User, email: "test@test.com", shipments: shipments) end

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

describe "ordering" do it "reorders shipments by their creation date" do 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

Slide 49

Slide 49 text

PAY ATTENTION WHEN CLASSES DO TOO MUCH

Slide 50

Slide 50 text

def process def serialize(shipment) def data(shipments) def headers def save_to_s3(file) def notify_user(email)

Slide 51

Slide 51 text

TESTS AS DOCUMENTATION

Slide 52

Slide 52 text

OPTIMIZE TESTS FOR HUMAN UNDERSTANDING

Slide 53

Slide 53 text

it "matches the expected output" do expect(csv_data).to eq(output_records) end

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

it "serializes users as passed into the service" do users = create_list(:user, 3) response = subject.new(User.all).process response = response[0][4] expect(response).to eq(users.first.name) end

Slide 56

Slide 56 text

it "serializes users as passed into the service" do users = create_list(:user, 3) serialized_users = subject.new.serialize(users) response = serialized_users.first.full_name expect(response).to eq(users.first.name) end

Slide 57

Slide 57 text

PAY ATTENTION TO SIMILARITY AND DIFFERENCE

Slide 58

Slide 58 text

let(:product_1) { create(:product) } let(:product_2) { create(:product) } let(:product_3) { create(:product) } let(:product_4) { create(:product) }

Slide 59

Slide 59 text

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) }

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

# 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

Slide 64

Slide 64 text

TRY TO STICK TO A GIVEN/ WHEN/THEN STRUCTURE

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

USING G/W/T EMPHASIZES SAMENESS AND DIFFERENCE.

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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) }

Slide 73

Slide 73 text

TESTS DON’T NEED TO BE (TOO) DRY

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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 } # ... lots of other tests ... it "returns order units summed, divided by units per shipper" do expect(subject.total_shipper_count).to eq(expected_total) end end end

Slide 76

Slide 76 text

context "rounding" do let!(:order) { create_store_order(count_units: 7) } let!(:expected_total) { 2 } # ... lots of other tests ... it "returns order units summed, divided by units per shipper" do expect(subject.total_shipper_count).to eq(expected_total) end end

Slide 77

Slide 77 text

IF YOUR TESTS ARE SUPER DUPLICATIVE, THAT’S ACTUALLY DESIGN FEEDBACK

Slide 78

Slide 78 text

SAY WHAT YOU MEAN, LITERALLY

Slide 79

Slide 79 text

def user_summary(user) "(#{user.id}) #{user.full_name}, #{user.role}" end

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

LEVERAGE YOUR TOOLS TO HELP GUIDE READERS

Slide 83

Slide 83 text

describe "#process" do it “has a correct return value" end describe "#process" do it "returns the number of records correctly persisted" end

Slide 84

Slide 84 text

describe CreateShipmentHistoryReport do describe "#sorted_shipments" do it "returns an empty array when there are no shipments" end end

Slide 85

Slide 85 text

rspec ./spec/services/create_shipment_history_report_spec.rb --format=‘documentation’ CreateShipmentHistoryReport#sorted_shipments returns an empty array when there are no shipments

Slide 86

Slide 86 text

it “picks randomly, but correctly” do double1 = double double2 = double result = [double1, double2].sample expect(result).to eq(double1) end # expected: # # got: #

Slide 87

Slide 87 text

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: # # got: #

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

TESTS AS VERIFICATION

Slide 93

Slide 93 text

TEST WHAT’S COMPLICATED, RISKY, OR CHEAP

Slide 94

Slide 94 text

YOU CAN’T HAVE 100% COVERAGE

Slide 95

Slide 95 text

DON’T TEST WHAT YOU DON’T OWN

Slide 96

Slide 96 text

class JobLog < ActiveRecord::Base validate :status, presence: true end it { should validate_presence_of(:status) }

Slide 97

Slide 97 text

class JobLog < ActiveRecord::Base validate :failure_message, presence: true, if: :failed? end it "requires failure_message for failures” do job = JobLog.new(status: :failed) expect(job).to validate_presence_of(:failure_message) job = JobLog.new(status: :completed) expect(job).not_to validate_presence_of(:failure_message) end

Slide 98

Slide 98 text

DON’T BE AFRAID TO DELETE YOUR TESTS

Slide 99

Slide 99 text

it "generates an array of strings” do expect(csv_data.length).to eq(shipments.length) expect(csv_data).to all(be_an(Array)) end

Slide 100

Slide 100 text

BEWARE OF OVER-MOCKING

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

IN SUMMARY!

Slide 105

Slide 105 text

IT GETS EASIER WITH PRACTICE

Slide 106

Slide 106 text

2. WRITE TESTS FOR HUMANS 3. VERIFICATION COMES LAST 1. USE TESTS TO DRIVE DESIGN

Slide 107

Slide 107 text

THANKS!