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

Testing 201, or: Great Expectations

Testing 201, or: Great Expectations

Ruby developers are a testy bunch. Who else writes more tests that we do? Which makes sense! Our language allows us freedom, and the opportunity to abuse it.

Most of us learned to test informally, from existing projects or tutorials. Sadly, that often leads us to the false conclusion that is all about verifying that our code works. Would you believe that’s only the third most important reason to test?

Let’s take a non-dogmatic look at the less obvious roles for testing with some real examples. We can get so much more from testing.

Joseph Mastey

August 04, 2020
Tweet

More Decks by Joseph Mastey

Other Decks in Programming

Transcript

  1. TESTING 201
    OR: GREAT EXPECTATIONS
    JOE MASTEY, AUGUST 2020

    View full-size slide

  2. LOTS OF RSPEC,
    FACTORY BOT,
    AND RAILS

    View full-size slide

  3. EVERYONE IS
    DOING THEIR BEST

    View full-size slide

  4. HEURISTICS,
    NOT RULES

    View full-size slide

  5. PROLOGUE: THE TROUBLE
    WITH A SIMPLE TEST

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  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

    View full-size slide

  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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. FOCUSING ON THE
    WRONG THINGS

    View full-size slide

  22. THE 3 ROLES
    OF TESTING (IN ORDER)

    View full-size slide

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

    View full-size slide

  24. TESTS AS
    DESIGN FEEDBACK

    View full-size slide

  25. A NOTE ON TDD

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  49. PAY ATTENTION WHEN
    CLASSES DO TOO MUCH

    View full-size slide

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

    View full-size slide

  51. TESTS AS
    DOCUMENTATION

    View full-size slide

  52. OPTIMIZE TESTS FOR
    HUMAN UNDERSTANDING

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  57. PAY ATTENTION TO
    SIMILARITY AND DIFFERENCE

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  64. TRY TO STICK TO A GIVEN/
    WHEN/THEN STRUCTURE

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  69. USING G/W/T EMPHASIZES
    SAMENESS AND
    DIFFERENCE.

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  73. TESTS DON’T NEED TO BE
    (TOO) DRY

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  78. SAY WHAT YOU MEAN,
    LITERALLY

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  82. LEVERAGE YOUR TOOLS TO
    HELP GUIDE READERS

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  92. TESTS AS
    VERIFICATION

    View full-size slide

  93. TEST WHAT’S COMPLICATED,
    RISKY, OR CHEAP

    View full-size slide

  94. YOU CAN’T HAVE
    100% COVERAGE

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  98. DON’T BE AFRAID TO
    DELETE YOUR TESTS

    View full-size slide

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

    View full-size slide

  100. BEWARE OF
    OVER-MOCKING

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  104. IT GETS EASIER
    WITH PRACTICE

    View full-size slide

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

    View full-size slide