Slide 1

Slide 1 text

TESTING 201 OR: GREAT EXPECTATIONS JOE MASTEY, APRIL 2019

Slide 2

Slide 2 text

EVERYONE IS DOING THEIR BEST

Slide 3

Slide 3 text

HEURISTICS, NOT RULES

Slide 4

Slide 4 text

THIS STUFF SHOULDN’T SLOW YOU DOWN

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 "generates an output record for each input, sorted by date" 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.subscription << subscription end

Slide 14

Slide 14 text

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

Slide 15

Slide 15 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 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!(: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 18

Slide 18 text

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

Slide 19

Slide 19 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 20

Slide 20 text

it "generates an output record for each input, sorted by date" do expect(csv_data).to eq(output_records) end

Slide 21

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

Slide 22

Slide 22 text

FOCUSING ON THE WRONG THINGS

Slide 23

Slide 23 text

THE 3 ROLES OF TESTING (IN ORDER)

Slide 24

Slide 24 text

1. DESIGN FEEDBACK 2. DOCUMENTATION 3. VERIFICATION

Slide 25

Slide 25 text

TESTS AS DESIGN FEEDBACK

Slide 26

Slide 26 text

A NOTE ON TDD

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 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 30

Slide 30 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 31

Slide 31 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 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 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 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 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 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 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 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 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 47

Slide 47 text

let(:user) do instance_double(User, email: "[email protected]", shipments: shipments) end

Slide 48

Slide 48 text

let(:user) { User.new(email: "[email protected]", name: "Joe") }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

PAY ATTENTION TO SIMILARITY AND DIFFERENCE

Slide 51

Slide 51 text

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

Slide 52

Slide 52 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 53

Slide 53 text

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

Slide 54

Slide 54 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 55

Slide 55 text

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

Slide 56

Slide 56 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 57

Slide 57 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 58

Slide 58 text

TRY TO REDUCE COMPLEXITY, NOT HIDE IT

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

it "generates an output record for each input, sorted by date" do expect(csv_data).to eq(output_records) end

Slide 61

Slide 61 text

DON’T DO TOO MUCH WORK

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

No content

Slide 64

Slide 64 text

TESTS AS DOCUMENTATION

Slide 65

Slide 65 text

WRITE TESTS FOR HUMANS, NOT FOR THE COMPILER

Slide 66

Slide 66 text

it "generates an output record for each input, sorted by date" do expect(csv_data).to eq(output_records) end

Slide 67

Slide 67 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 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

TRY TO STICK TO A GIVEN/ WHEN/THEN STRUCTURE

Slide 71

Slide 71 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 72

Slide 72 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 73

Slide 73 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 74

Slide 74 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 75

Slide 75 text

USING G/W/T EMPHASIZES SAMENESS AND DIFFERENCE.

Slide 76

Slide 76 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 77

Slide 77 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 78

Slide 78 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 79

Slide 79 text

TESTS DON’T NEED TO BE (TOO) DRY

Slide 80

Slide 80 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 81

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

subject(:service) { described_class.new } it "sends email" do expect(service).to receive(:notify_user) end after do service.process end

Slide 84

Slide 84 text

THE FEELING OF NEEDING TO DRY YOUR TESTS IS ACTUALLY DESIGN FEEDBACK

Slide 85

Slide 85 text

SAY WHAT YOU MEAN, LITERALLY

Slide 86

Slide 86 text

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

Slide 87

Slide 87 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 88

Slide 88 text

LEVERAGE YOUR TOOLS TO HELP GUIDE READERS

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 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 93

Slide 93 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 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 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 97

Slide 97 text

No content

Slide 98

Slide 98 text

TESTS AS VERIFICATION

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

YOU CAN’T HAVE 100% COVERAGE

Slide 101

Slide 101 text

DON’T TEST WHAT YOU DON’T OWN

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

DON’T BE AFRAID TO DELETE YOUR TESTS

Slide 105

Slide 105 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 106

Slide 106 text

BEWARE OF OVER-MOCKING

Slide 107

Slide 107 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 108

Slide 108 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 109

Slide 109 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 110

Slide 110 text

No content

Slide 111

Slide 111 text

IN SUMMARY!

Slide 112

Slide 112 text

2. ACTUALLY, JUST DELETE THEM 3. TINY PIGS ARE WAY CUTER THAN CATS 1. KEEP YOUR TESTS MOIST

Slide 113

Slide 113 text

2. WRITE TESTS FOR HUMANS 3. TINY PIGS ARE WAY CUTER THAN CATS 1. USE TESTS TO DRIVE DESIGN

Slide 114

Slide 114 text

THANKS!