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

Tests Should Tell a Story

Tests Should Tell a Story

Given at Laracon US 2016.

Matthew Machuga

July 28, 2016
Tweet

More Decks by Matthew Machuga

Other Decks in Programming

Transcript

  1. TESTS SHOULD TELL A STORY
    LARACON US 2016

    View Slide

  2. STORIES

    View Slide

  3. WHO KNOWS THIS MAN?

    View Slide

  4. FRED ROGERS

    View Slide

  5. THE NEIGHBORHOOD LIVES ON

    View Slide

  6. We live in a world in which
    we need to share
    responsibility.
    It's easy to say, “It's not my
    child, not my community,
    not my world, not my
    problem.”
    Then there are those who
    see the need and respond. I
    consider those people my
    heroes.
    Fred Rogers

    View Slide

  7. TAYLOR OTWELL

    View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. View Slide

  12. YOUR CODE
    TELLS A STORY

    View Slide

  13. View Slide

  14. TESTS SHOULD TELL A STORY
    DISTILLED SHARED KNOWLEDGE
    ▸ Good naming
    ▸ Good abstractions
    ▸ Details pushed down
    ▸ Intent clearly expressed
    ▸ Test driven (mostly)

    View Slide

  15. TESTS SHOULD TELL A STORY
    IN STORY FORM
    ▸ Who are the characters? (Subject & Collaborators)
    ▸ What is the setting? (Environment)
    ▸ What is the plot? (Goal of our system)
    ▸ What is the conflict? (Task at hand)
    ▸ What is the resolution? (Solution)

    View Slide

  16. STORIES ARE NOT JUST TOLD
    THEY ARE INTERPRETED

    View Slide

  17. I NEED MAYO
    Coheed and Cambria
    HEADSHOT KID DISASTER FROM SECOND STAGE TURBINE BLADE

    View Slide

  18. WHAT DO YOU SEE?

    View Slide

  19. View Slide

  20. EVEN FUTURE-YOU WILL
    INTERPRET CODE DIFFERENTLY

    View Slide

  21. TESTS SHOULD TELL A STORY
    COMMUNICATIVE CONCERNS TO CONSIDER
    ▸ Skill level
    ▸ Ambiguous intent
    ▸ Cultural differences
    ▸ Language differences
    ▸ Contextual differences
    ▸ Mood at the time of reading
    ▸ Knowledge of area/domain of code

    View Slide

  22. View Slide

  23. TESTS SHOULD TELL A STORY
    EXPECTATIONS
    ▸ Ex: A method named read in a CSV parsing library
    ▸ Load the entire file into memory into an array
    ▸ Stream the file, each line as an array
    ▸ Load the file into a lazily evaluated collection - each row
    lazily loaded in an array

    View Slide

  24. AMBIGUITY LEAVES ROOM FOR
    INTERPRETATION

    View Slide

  25. We don’t make
    mistakes, we
    have happy
    accidents.
    Bob Ross

    View Slide

  26. WE ARE NOT BOB ROSS

    View Slide

  27. WE MAKE MISTAKES

    View Slide

  28. OUR MISTAKES HAVE
    CONSEQUENCES

    View Slide

  29. WRITE TESTS
    TO PREVENT HAPPY ACCIDENTS

    View Slide

  30. YOU DO WRITE TESTS, DON’T YOU?

    View Slide

  31. ಠ_ಠ

    View Slide

  32. ಠ_ಠ

    View Slide

  33. ಠ_ಠ

    View Slide

  34. View Slide

  35. KEYNOTE !"

    View Slide

  36. View Slide

  37. TESTS

    View Slide

  38. ANATOMY OF A TEST

    View Slide

  39. TESTS SHOULD TELL A STORY
    ANATOMY OF A TEST - FOUR PHASE
    ▸ Setup environment and preconditions
    ▸ Exercise the method/system under test
    ▸ Verify the result
    ▸ Tear down the environment

    View Slide

  40. TESTS SHOULD TELL A STORY
    ANATOMY OF A TEST - AAA
    ▸ Arrange preconditions and input
    ▸ Act on the object/method/system under test
    ▸ Assert what you expect the result to be

    View Slide

  41. TESTS SHOULD TELL A STORY
    ANATOMY OF A TEST - GIVEN-WHEN-THEN
    ▸ Arrange - Given
    ▸ Act - When
    ▸ Assert - Then

    View Slide

  42. SETTING A BASELINE

    View Slide

  43. TESTS SHOULD TELL A STORY
    PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  44. XUNIT

    View Slide

  45. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  46. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  47. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  48. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  49. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  50. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  51. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  52. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  53. SPEC/BDD STYLE

    View Slide

  54. AT A BASIC LEVEL
    VERY SIMILAR

    View Slide

  55. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  56. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  57. TESTS SHOULD TELL A STORY
    XUNIT - PHPUNIT
    class UserRepositoryTest extends PHPUnit_Framework_TestCase {
    public function setup() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    }
    public function teardown() {
    $this->subject->clear();
    }
    public function testCanFindAUserByName() {
    $user = $this->subject->findByName($this->name);
    $this->assertEquals($this->name, $user->name);
    }
    }

    View Slide

  58. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  59. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  60. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  61. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  62. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  63. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  64. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  65. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  66. TESTS SHOULD TELL A STORY
    SPEC/BDD - PHO
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    it(‘can find a user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    });

    View Slide

  67. TESTS SHOULD TELL A STORY
    NESTED CONTEXTS
    describe(‘A User Repository’, function() {
    beforeEach(function() {
    $this->subject = new UserRepository();
    $this->name = ‘Machuga’;
    $this->subject->save(new User($this->name));
    });
    afterEach(function() {
    $this->subject->clear();
    });
    context(‘when finding a user’, function() {
    it(‘can find an existing user by name’, function() {
    $user = $this->subject->findByName($this->name);
    expect($user->name)->toEqual($this->name);
    });
    it(‘returns null when given an invalid name’, function() {
    $user = $this->subject->findByName(‘Santa Claus’);
    expect($user)->toEqual(null);
    });
    });
    });

    View Slide

  68. TESTS SHOULD TELL A STORY
    SPEC OR DOCUMENTATION FORMATTER PRINTS AS
    A User Repository
    when finding a user
    can find an existing user by name
    returns null when given an invalid username

    View Slide

  69. TESTS SHOULD TELL A STORY
    SUBJECTIVE PREFERENCE TO BDD SPECS OVER XUNIT
    ▸ Generally written out in sentences
    ▸ Easily mapped to user stories
    ▸ Easily mapped to acceptance criteria
    ▸ Easily readable by stakeholders
    ▸ Allow for contexts to be described easily
    ▸ Allow for nested contexts
    ▸ Handles multiple cases well
    ▸ I work with them day-to-day

    View Slide

  70. HOW ARE YOU FEELING
    ABOUT THE SPEC DSL?

    View Slide

  71. HOW ARE YOU FEELING
    ABOUT THE SPEC DSL?

    View Slide

  72. SPEC LIBRARIES
    WE’LL BE USING

    View Slide

  73. SPEC LIBRARIES
    WE’LL BE USING

    View Slide

  74. JASMINE
    &
    RSPEC

    View Slide

  75. TESTS SHOULD TELL A STORY
    JASMINE
    describe(‘A User Repository’, function() {
    const subject = new UserRepository();
    const name = ‘Machuga’;
    beforeEach(function() {
    subject.save(new User(name));
    });
    afterEach(function() {
    subject.clear();
    });
    it(‘can find a user by name’, function() {
    const user = subject.findByName(name);
    expect(user.name).toEqual(name);
    });
    });

    View Slide

  76. TESTS SHOULD TELL A STORY
    JASMINE
    describe(‘A User Repository’, function() {
    const subject = new UserRepository();
    const name = ‘Machuga’;
    beforeEach(function() {
    subject.save(new User(name));
    });
    afterEach(function() {
    subject.clear();
    });
    it(‘can find a user by name’, function() {
    const user = subject.findByName(name);
    expect(user.name).toEqual(name);
    });
    });
    const defines a variable
    that cannot be redefined

    View Slide

  77. TESTS SHOULD TELL A STORY
    JASMINE
    describe(‘A User Repository’, function() {
    const subject = new UserRepository();
    const name = ‘Machuga’;
    beforeEach(function() {
    subject.save(new User(name));
    });
    afterEach(function() {
    subject.clear();
    });
    it(‘can find a user by name’, function() {
    const user = subject.findByName(name);
    expect(user.name).toEqual(name);
    });
    });
    dot syntax rather than ->

    View Slide

  78. TESTS SHOULD TELL A STORY
    JASMINE
    describe(‘A User Repository’, function() {
    const subject = new UserRepository();
    const name = ‘Machuga’;
    beforeEach(function() {
    subject.save(new User(name));
    });
    afterEach(function() {
    subject.clear();
    });
    it(‘can find a user by name’, function() {
    const user = subject.findByName(name);
    expect(user.name).toEqual(name);
    });
    });
    lexical scope for variables

    View Slide

  79. TESTS SHOULD TELL A STORY
    RSPEC
    describe ‘A User Repository’ do
    subject { UserRepository.new }
    let(:name) { ‘Machuga’ }
    before do
    subject.save(User.new(name))
    end
    after do
    subject.clear!
    end
    it ‘can find a user by name’ do
    user = subject.find_by_name(name)
    expect(user.name).to eq(name)
    end
    end

    View Slide

  80. TESTS SHOULD TELL A STORY
    RSPEC
    describe ‘A User Repository’ do
    subject { UserRepository.new }
    let(:name) { ‘Machuga’ }
    before do
    subject.save(User.new(name))
    end
    after do
    subject.clear!
    end
    it ‘can find a user by name’ do
    user = subject.find_by_name(name)
    expect(user.name).to eq(name)
    end
    end
    blocks denoted by { } or do..end

    View Slide

  81. TESTS SHOULD TELL A STORY
    RSPEC
    describe ‘A User Repository’ do
    subject { UserRepository.new }
    let(:name) { ‘Machuga’ }
    before do
    subject.save(User.new(name))
    end
    after do
    subject.clear!
    end
    it ‘can find a user by name’ do
    user = subject.find_by_name(name)
    expect(user.name).to eq(name)
    end
    end
    optional syntax

    View Slide

  82. TESTS SHOULD TELL A STORY
    RSPEC
    describe ‘A User Repository’ do
    subject { UserRepository.new }
    let(:name) { ‘Machuga’ }
    before do
    subject.save(User.new(name))
    end
    after do
    subject.clear!
    end
    it ‘can find a user by name’ do
    user = subject.find_by_name(name)
    expect(user.name).to eq(name)
    end
    end
    new is a method on the class

    View Slide

  83. TESTS SHOULD TELL A STORY
    RSPEC
    describe ‘A User Repository’ do
    subject { UserRepository.new }
    let(:name) { ‘Machuga’ }
    before do
    subject.save(User.new(name))
    end
    after do
    subject.clear!
    end
    it ‘can find a user by name’ do
    user = subject.find_by_name(name)
    expect(user.name).to eq(name)
    end
    end
    can use ! and ? in method names

    View Slide

  84. 5 TIPS FOR SPEC
    STORYTELLING
    OR: I TRIED THESE 5 WEIRD TRICKS ON MY TESTS;
    YOU’LL NEVER GUESS WHAT HAPPENED NEXT!

    View Slide

  85. 1. BE EXPRESSIVE

    View Slide

  86. CUSTOM ACTIONS
    FOR COMPLEX BEHAVIOR

    View Slide

  87. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST
    describe 'items' do

    it 'should be able to be dragged into categories' do

    drag_item_to_category_with_text 'apple', 'a-f'
    expect('apple').to be_in_category_with_text(‘a-f')

    end

    end

    View Slide

  88. TESTS SHOULD TELL A STORY
    LAYOUT
    CHECK NEXT

    View Slide

  89. TESTS SHOULD TELL A STORY
    LAYOUT
    CHECK NEXT

    View Slide

  90. TESTS SHOULD TELL A STORY
    RSPEC CUSTOM ACTION
    # Perform drag and drop on item by name to category by name

    def drag_item_to_category_with_text(item_text, category_text)

    item(item_text).drag_to category_with_text(category_text)

    end

    View Slide

  91. TESTS SHOULD TELL A STORY
    RSPEC CUSTOM ACTION
    # Perform drag and drop on item by name to category by name

    def drag_item_to_category_with_text(item_text, category_text)

    item(item_text).drag_to category_with_text(category_text)

    end

    def item(text)

    sel = “#{item_prefix}_categorizable_selector”

    page.find(send(sel), text: text)

    end

    def category_with_text(text)

    sel = “#{category_prefix}_category_selector”

    page.find(send(sel), text: text)

    end

    View Slide

  92. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST REPRISE
    describe 'items' do

    it 'should be able to be dragged into categories' do

    drag_item_to_category_with_text 'apple', 'a-f'
    expect('apple').to be_in_category_with_text(‘a-f')

    end

    end

    View Slide

  93. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST UNFOLDED
    describe 'items' do

    it 'should be able to be dragged into categories' do

    item = page.find(

    send(#{item_prefix}_categorizable_selector”),

    text: ‘apple’)

    category = page.find(

    send(“#{category_prefix}_category_selector”),

    text: ‘a-f’)


    item.drag_to category
    expect('apple').to be_in_category_with_text(‘a-f')

    end

    end

    View Slide

  94. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST DOUBLED DOWN
    describe 'items' do
    it 'should be able to be dragged into categories' do

    item1 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'apple')
    item2 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'oranges')
    item3 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'bananas')
    item4 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'kiwis')
    category = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'a-f')

    item1.drag_to category
    item2.drag_to category
    item3.drag_to category
    item4.drag_to category
    expect(uncategorized_items).to be_empty

    end

    end

    View Slide

  95. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST DOUBLED DOWN
    describe 'items' do
    it 'should be able to be dragged into categories' do

    item1 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'apple')
    item2 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'oranges')
    item3 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'bananas')
    item4 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'kiwis')
    category = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'a-f')

    item1.drag_to category
    item2.drag_to category
    item3.drag_to category
    item4.drag_to category
    expect(uncategorized_items).to be_empty

    end

    end

    View Slide

  96. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST NOPE
    describe 'items' do
    it 'should be able to be dragged into categories' do

    item1 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'apple')
    item2 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'oranges')
    item3 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'bananas')
    item4 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'kiwis')
    category1 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'a-f')

    category2 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'g-n')

    category3 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'o-w')

    category4 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'x-z')

    item1.drag_to category1
    item2.drag_to category2
    item3.drag_to category3
    item4.drag_to category4
    check_button.click
    expect(next_button).to be_enabled

    end

    end

    View Slide

  97. TESTS SHOULD TELL A STORY
    RSPEC ACCEPTANCE TEST NOPE
    describe 'items' do
    it 'should be able to be dragged into categories' do

    item1 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'apple')
    item2 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'oranges')
    item3 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'bananas')
    item4 = page.find(

    send("#{item_prefix}_categorizable_selector"),

    text: 'kiwis')
    category1 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'a-f')

    category2 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'g-n')

    category3 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'o-w')

    category4 = page.find(

    send('#{category_prefix}_category_selector'),

    text: 'x-z')

    item1.drag_to category1
    item2.drag_to category2
    item3.drag_to category3
    item4.drag_to category4
    check_button.click
    expect(next_button).to be_enabled

    end

    end

    View Slide

  98. TESTS ARE CODE
    REFACTOR!

    View Slide

  99. USE THE BEST
    MATCHER / ASSERTION

    View Slide

  100. expect((2+2).to_s == 4).to eq true

    View Slide

  101. Failures:
    1) Hi, Laracon! #
    Failure/Error: expect((2+2).to_s == 4).to eq true
    expected true
    got false
    (compared using ==)
    # ./testing_spec.rb:3:in `block (2 levels) in '
    Finished in 0.02861 seconds (files took 0.16021 seconds to load)
    1 example, 1 failure
    Failed examples:
    rspec ./testing_spec.rb:2 # My awesome example group works

    View Slide

  102. expect((2+2).to_s).to eq 4

    View Slide

  103. Failures:
    1) Hi, Laracon! #
    Failure/Error: expect((2+2).to_s).to eq 4
    expected: 4
    got: "4"
    (compared using ==)
    # ./testing_spec.rb:3:in `block (2 levels) in '
    Finished in 0.01195 seconds (files took 0.13384 seconds to load)
    1 example, 1 failure
    Failed examples:
    rspec ./testing_spec.rb:2 # My awesome example group works

    View Slide

  104. CUSTOM
    MATCHERS / ASSERTIONS

    View Slide

  105. TESTS SHOULD TELL A STORY
    LARAVEL CUSTOM ASSERTION
    /**
    * Asserts that the response contains the given header
    * and equals the optional value.
    *
    * @param string $headerName
    * @param mixed $value
    * @return $this
    */
    protected function seeHeader($headerName, $value = null)
    {
    $headers = $this->response->headers;
    $this->assertTrue($headers->has($headerName), "Header [{$headerName}] not present on
    response.");
    if (! is_null($value)) {
    $this->assertEquals(
    $headers->get($headerName), $value,
    "Header [{$headerName}] was found, but value [{$headers->get($headerName)}] does
    not match [{$value}]."
    );
    }
    return $this;
    }

    View Slide

  106. TESTS SHOULD TELL A STORY
    PHPUNIT CUSTOM ASSERTIONS BLESSED METHOD
    use PHPUnit\Framework\TestCase;
    abstract class PHPUnit_Framework_Assert {
    public static function assertTrue($condition, $message = ‘') {
    self::assertThat($condition, self::isTrue(), $message);
    }
    public static function isTrue()
    {
    return new PHPUnit_Framework_Constraint_IsTrue;
    }
    }
    class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint {
    public function matches($other) {
    return $other === true;
    }
    public function toString() {
    return 'is true';
    }
    }

    View Slide

  107. TESTS SHOULD TELL A STORY
    RSPEC CUSTOM MATCHER
    require 'rspec/expectations'
    RSpec::Matchers.define :be_a_multiple_of do |expected|
    match do |actual|
    actual % expected == 0
    end
    failure_message do |actual|
    "expected that #{actual} would be a multiple of #{expected}"
    end
    end
    RSpec.describe 9 do
    it ‘should blow up and be helpful’ do
    expect(9).to be_a_multiple_of(4)
    end
    end
    Failures:
    1) 9 should blow up and be helpful
    Failure/Error: expect(9).to be_a_multiple_of(4)
    expected that 9 would be a multiple of 4

    View Slide

  108. 2. RESPECT THE CUKES

    View Slide

  109. TESTS SHOULD TELL A STORY
    CUCUMBER

    View Slide

  110. TESTS SHOULD TELL A STORY
    USER STORIES AS GHERKIN SPECS
    Feature: Getting Laracon attendees test their code
    In order to convince Laracon attendees to test their code

    As a speaker

    I want to provide helpful information and entertainment
    Scenario: Adam Wathan gives a TDD talk prior to my talk

    Given that attendees recently saw Adam’s talk

    And I was in the audience

    When I go to give my talk

    Then I can make some assumptions on what attendees know

    And I can weep as a rearrange my slide deck

    View Slide

  111. TESTS SHOULD TELL A STORY
    USER STORIES AS RSPEC SPECS
    describe ‘Convincing Laracon attendees to test their code’ do
    describe ‘Giving a talk as a speaker’ do
    context ‘Adam Wathan gives a TDD talk the day before’ do
    it ‘can make some assumptions on what attendees know’
    it ‘can weep while rearranging slide deck’
    end
    end
    end

    View Slide

  112. TESTS SHOULD TELL A STORY
    BEHAVIOR-DRIVEN DEVELOPMENT
    ‣ Describing/designing an application as how it appears to the
    stakeholder
    ‣ Feature-driven workflow
    ‣ Embraces concepts of the Agile Manifesto
    ‣ Individuals and interactions over processes and tools
    ‣ Working software over comprehensive documentation
    ‣ Customer collaboration over contract negotiation
    ‣ Responding to change over following a plan

    View Slide

  113. 3. BAD STORIES ARE SIGNALS

    View Slide

  114. TESTS SHOULD TELL A STORY
    SMELLS OF BAD STORIES
    ▸ Plot holes
    ▸ Lack of Character Development
    ▸ No Descriptions
    ▸ Switching narrative

    View Slide

  115. TESTS SHOULD TELL A STORY
    PLOT HOLES
    ▸ Mocking the object under test
    ▸ Global state mutations out of view
    ▸ Implicit Behavior

    View Slide

  116. TESTS SHOULD TELL A STORY
    NO DESCRIPTIONS
    ▸ Creating abstractions that provide no benefit
    ▸ No explanation why the environment is in current state
    ▸ The world is bland and empty

    View Slide

  117. TESTS SHOULD TELL A STORY
    LACK OF CHARACTER DEVELOPMENT
    ▸ Performing transformations on characters out of view
    ▸ Mocking collaborators without clear reason

    View Slide

  118. TESTS SHOULD TELL A STORY
    SWITCHING NARRATIVES
    ▸ Setting expectations on collaborators
    ▸ Blurring lines between suites

    View Slide

  119. 4. UNIT TEST IN ISOLATION

    View Slide

  120. MORE ON THIS SOON

    View Slide

  121. 5. TRY TDD/BDD

    View Slide

  122. TWO WEEKS

    View Slide

  123. TESTS SHOULD TELL A STORY
    FEATURE-FIRST
    ▸ Build only what your client needs at that moment
    ▸ Supports slice plans
    ▸ Waste few resources to test an idea
    ▸ Provides readable, agreed upon specs for stakeholders
    ▸ Encourages collaboration

    View Slide

  124. TESTS SHOULD TELL A STORY
    DRIVING YOUR DESIGN & CODE

    View Slide

  125. TESTS SHOULD TELL A STORY
    DRIVING YOUR DESIGN & CODE
    ▸ Write failing test first
    ▸ Evidence feature is incomplete
    ▸ Evidence feature is not coincidently passing

    View Slide

  126. TESTS SHOULD TELL A STORY
    DRIVING YOUR DESIGN & CODE
    ▸ Write failing test first
    ▸ Evidence feature is incomplete
    ▸ Evidence feature is not coincidently passing
    ▸ Write smallest amount of code
    ▸ Evidence necessary code to solve that use-case is implemented

    View Slide

  127. TESTS SHOULD TELL A STORY
    DRIVING YOUR DESIGN & CODE
    ▸ Write failing test first
    ▸ Evidence feature is incomplete
    ▸ Evidence feature is not coincidently passing
    ▸ Write smallest amount of code
    ▸ Evidence necessary code to solve that use-case is implemented
    ▸ Listen to feedback & refactor code & test in pieces
    ▸ Confidence nothing is broken
    ▸ Aids in better design

    View Slide

  128. TESTS SHOULD TELL A STORY
    DRIVING YOUR DESIGN & CODE
    ▸ Write failing test first
    ▸ Evidence feature is incomplete
    ▸ Evidence feature is not coincidently passing
    ▸ Write smallest amount of code
    ▸ Evidence necessary code to solve that use-case is implemented
    ▸ Listen to feedback & refactor code & test in pieces
    ▸ Confidence nothing is broken
    ▸ Aids in better design
    ▸ Taking small steps
    ▸ Ensures frequent save points

    View Slide

  129. TIME FOR A STORY
    WARNING: MATH AHEAD

    View Slide

  130. View Slide

  131. TESTS SHOULD TELL A STORY
    LMS
    ▸ Per School Year
    ▸ ~ 3 Million Students
    ▸ ~ 2 Million Live Help Sessions
    ▸ ~ 1 Billion Math Problems
    ▸ Math Content Quantity Comparable to Khan Academy
    ▸ A gigantic Rails monolith + two SPA’s

    View Slide

  132. View Slide

  133. TESTS SHOULD TELL A STORY
    LIVE HELP
    ▸ Teachers available to help students
    ▸ Voice
    ▸ Text
    ▸ Whiteboard

    View Slide

  134. View Slide

  135. View Slide

  136. TESTS SHOULD TELL A STORY
    LIVE HELP
    ▸ Originally in Flash
    ▸ Reimagined & Rewritten in 2013
    ▸ Goals of rewrite
    ▸ Decrease wait time of over 2 minutes
    ▸ Work on iPad
    ▸ Improve performance and maintainability
    ▸ Better pairing of teachers to students

    View Slide

  137. TESTS SHOULD TELL A STORY
    LIVE HELP
    ▸ Rewrote in CoffeeScript with Node & Angular
    ▸ Great Success
    ▸ Throughput bumped to the current level
    ▸ Wait times decreased to typically < 15 seconds
    ▸ Compatible with iPad
    ▸ Improved pairing students to teachers

    View Slide

  138. TESTS SHOULD TELL A STORY
    LIVE HELP
    ▸ Left largely untouched until big update in late 2015
    ▸ Compiled all CoffeeScript to JS, switched to ES2015
    ▸ Simplified server-side to boost performance + scaling

    View Slide

  139. THE PAIRING ALGORITHM

    View Slide

  140. View Slide

  141. TESTS SHOULD TELL A STORY
    LIVE HELP PAIRING ALGORITHM
    ▸ Time-based weighting queue
    ▸ Grows linearly over time
    ▸ Those who are waiting longer get selected first
    ▸ Bonus weighting for teachers/students speaking same language
    ▸ Adds large offset that tapers over time
    ▸ Offset becomes 0 at threshold
    ▸ Fastpass match weight - Guaranteed win

    View Slide

  142. TESTS SHOULD TELL A STORY
    GRAPHS!
    Weight Points
    0
    5000
    10000
    15000
    20000
    Seconds
    0 5 10 15 20
    Student w/ Language Mismatch
    Student w/ Language Match

    View Slide

  143. TESTS SHOULD TELL A STORY
    LIVE HELP PAIRING ALGORITHM - WITHIN THRESHOLD
    Given a language boost threshold of 15000

    And EnglishStudent1 has waited 11s

    And SpanishStudent1 has waited 6s (13.5s)

    When a Spanish Teacher pulls in another student

    Then that student will be SpanishStudent1
    Weight
    Points
    0
    15000
    30000
    Seconds
    0 5 10 15 20 25 30
    English Student Spanish Student

    View Slide

  144. TESTS SHOULD TELL A STORY
    LIVE HELP PAIRING ALGORITHM - WITHIN THRESHOLD
    Given a language boost threshold of 15000

    And EnglishStudent1 has waited 11s

    And SpanishStudent1 has waited 6s (13.5s)

    When a Spanish Teacher pulls in another student

    Then that student will be SpanishStudent1
    Weight
    Points
    0
    15000
    30000
    Seconds
    0 5 10 15 20 25 30
    English Student Spanish Student

    View Slide

  145. TESTS SHOULD TELL A STORY
    LIVE HELP PAIRING ALGORITHM - PAST THRESHOLD
    Given a language boost threshold of 15000

    And EnglishStudent1 has waited 20s

    And SpanishStudent1 has waited 15s (15s)

    When a Spanish Teacher pulls in another student

    Then that student will be EnglishStudent1
    Weight
    Points
    0
    15000
    30000
    Seconds
    0 5 10 15 20 25 30
    English Student Spanish Student

    View Slide

  146. REFACTORING SPECS

    View Slide

  147. TESTS SHOULD TELL A STORY
    PREVIOUS SPEC (COMPILED FROM COFFEESCRIPT)
    describe("PairMatcher", function() {
    describe("Pair scoring order", function() {
    describe("Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(), this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(), this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    var match1, match2;
    this.offsetStudentTimes();
    match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe('es');
    expect(match2.getLanguage()).toBe('es');
    });
    });
    });
    });

    View Slide

  148. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  149. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  150. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  151. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    WHAT LANGUAGE DOES
    STUDENT SPEAK?

    View Slide

  152. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  153. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  154. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    WTF IS THIS?

    View Slide

  155. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    WTF IS THIS?
    ADD OPTIONAL OFFSET

    STAGGER WAIT TIMES

    REVERSE CHRONOLOGICALLY

    View Slide

  156. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  157. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    WUT?

    View Slide

  158. TESTS SHOULD TELL A STORY
    EXISTING SPEC
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });

    View Slide

  159. TESTS SHOULD TELL A STORY
    THE OLD STORY
    Describe the pair order for Spanish speaking teachers
    Given this list of four students [es, en, es, en]

    And all have wait times within the language threshold

    And each is waiting longer than the previous

    When the teacher pulls two new students

    Then the teacher should receive the Spanish speaking students

    View Slide

  160. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    THE PROLOGUE

    View Slide

  161. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    THE SUPPORTING CAST

    View Slide

  162. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    THE PROTAGONIST

    View Slide

  163. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    THE PLOT

    View Slide

  164. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    M. NIGHT SHYMALAN TWIST &

    NO CHARACTER DEVELOPMENT

    View Slide

  165. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    THE CONFLICT

    View Slide

  166. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    PLOT HOLE

    View Slide

  167. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    BREAKING DOWN THE 4TH WALL

    View Slide

  168. TESTS SHOULD TELL A STORY
    THE OLD STORY
    describe(“PairMatcher Pair scoring order for Spanish speaking teachers", function() {
    beforeEach(function() {
    this.students = [
    this.helper.createSpanishStudent(),
    this.helper.createEnglishStudent(),
    this.helper.createSpanishStudent(),
    this.helper.createStudent()
    ];
    this.matcher = new PairMatcher(this.students);
    });
    it("will prefer new Spanish speaking students", function() {
    this.offsetStudentTimes();
    var match1 = this.matcher.getNextStudentFor(this.spanishTeacher);
    this.removeStudent(match1);
    var match2 = this.matcher.getNextStudentFor(this.spanishTeacher);
    expect(match1.getLanguage()).toBe(‘es’);
    expect(match2.getLanguage()).toBe('es');
    });
    });
    THE RESOLUTION

    View Slide

  169. TESTS SHOULD TELL A STORY
    GOOD THINGS ABOUT THIS SPEC

    View Slide

  170. TESTS SHOULD TELL A STORY
    GOOD THINGS ABOUT THIS SPEC
    ▸ Mostly clear name for example group and test description

    View Slide

  171. TESTS SHOULD TELL A STORY
    GOOD THINGS ABOUT THIS SPEC
    ▸ Mostly clear name for example group and test description
    ▸ Using this (disposable context in Jasmine)

    View Slide

  172. TESTS SHOULD TELL A STORY
    GOOD THINGS ABOUT THIS SPEC
    ▸ Mostly clear name for example group and test description
    ▸ Using this (disposable context in Jasmine)
    ▸ Assertions match description

    View Slide

  173. TESTS SHOULD TELL A STORY
    NOT-SO-GOOD THINGS ABOUT THIS SPEC

    View Slide

  174. TESTS SHOULD TELL A STORY
    NOT-SO-GOOD THINGS ABOUT THIS SPEC
    ▸ Helpers cause side-effects (offsetTimes, removeStudent)

    View Slide

  175. TESTS SHOULD TELL A STORY
    NOT-SO-GOOD THINGS ABOUT THIS SPEC
    ▸ Helpers cause side-effects (offsetTimes, removeStudent)
    ▸ Object under test has dependency mutated after use

    View Slide

  176. TESTS SHOULD TELL A STORY
    NOT-SO-GOOD THINGS ABOUT THIS SPEC
    ▸ Helpers cause side-effects (offsetTimes, removeStudent)
    ▸ Object under test has dependency mutated after use
    ▸ Character development happens off-screen

    View Slide

  177. TESTS SHOULD TELL A STORY
    NOT-SO-GOOD THINGS ABOUT THIS SPEC
    ▸ Helpers cause side-effects (offsetTimes, removeStudent)
    ▸ Object under test has dependency mutated after use
    ▸ Character development happens off-screen
    ▸ Expectations are dependent on result of side-effects

    View Slide

  178. TESTS SHOULD TELL A STORY
    NOT-SO-GOOD THINGS ABOUT THIS SPEC
    ▸ Helpers cause side-effects (offsetTimes, removeStudent)
    ▸ Object under test has dependency mutated after use
    ▸ Character development happens off-screen
    ▸ Expectations are dependent on result of side-effects
    ▸ Three methods of creating a student are used

    View Slide

  179. TESTS PASS
    SPANISH STUDENTS ARE PULLED FIRST

    View Slide

  180. TESTS SHOULD TELL A STORY
    THE NEW STORY
    Describe A Spanish Speaking teacher pulling students with
    the same language
    Given all wait times are within the threshold

    And englishStudent1 has been waiting 3s

    And spanishStudent2 has been waiting 2s

    And spanishStudent1 has been waiting 1s

    When the teacher pulls the next student

    Then the teacher should receive spanishStudent2

    View Slide

  181. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });

    View Slide

  182. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });
    THE PROLOGUE

    View Slide

  183. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });
    CHARACTER DEVELOPMENT

    View Slide

  184. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });
    THE CHOSEN ONE

    View Slide

  185. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });
    THE PLOT

    View Slide

  186. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });
    THE CONFLICT

    View Slide

  187. TESTS SHOULD TELL A STORY
    THE NEW STORY
    describe('StudentTeacherScorer', function() {
    var sessions = [];
    //…
    describe('A teacher speaking same language as a student', function() {
    beforeEach(function() {
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;
    englishStudent1.waitTime = 3000;
    sessions.push(englishStudent1);
    sessions.push(spanishStudent2);
    sessions.push(spanishStudent1);
    });
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    });
    });
    THE RESOLUTION

    View Slide

  188. TESTS FAIL

    View Slide

  189. TESTS FAIL
    Expected: spanishStudent2
    Actual: spanishStudent1

    View Slide

  190. View Slide

  191. spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;

    View Slide

  192. 1000 > 2000?
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;

    View Slide

  193. 1000 > 2000?
    spanishStudent1.waitTime = 1000;
    spanishStudent2.waitTime = 2000;

    View Slide

  194. EXISTING BEHAVIOR
    VERIFIED BY RUNNING THE OLD SPEC
    WITH NEW ASSERTIONS

    View Slide

  195. A WILD BUG APPEARS
    MATH TO THE RESCUE!

    View Slide

  196. TESTS SHOULD TELL A STORY
    I AM AWFUL AT SPREADSHEETS
    Score w/ Language
    Language Weight
    Normal Score
    X-axis: Time
    Y-axis: Score

    View Slide

  197. TESTS SHOULD TELL A STORY
    I AM AWFUL AT SPREADSHEETS
    Lang score
    decreased
    over time till
    negated
    Score w/ Language
    Language Weight
    Normal Score
    X-axis: Time
    Y-axis: Score

    View Slide

  198. TESTS SHOULD TELL A STORY
    I AM AWFUL AT SPREADSHEETS
    Score should
    always
    increase
    Lang score
    decreased
    over time till
    negated
    Score w/ Language
    Language Weight
    Normal Score
    X-axis: Time
    Y-axis: Score

    View Slide

  199. WHAT DID WE DO ABOUT IT?

    View Slide

  200. TESTS SHOULD TELL A STORY
    HACK THE PLANET
    it('will pick the student waiting longest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });

    View Slide

  201. TESTS SHOULD TELL A STORY
    HACK THE PLANET
    it('will pick the student waiting shortestlongest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });

    View Slide

  202. TESTS SHOULD TELL A STORY
    HACK THE PLANET
    it('will pick the student waiting shortestlongest', function() {
    const chosenStudent = scorer.findBestMatchFor(sessions, spanishTeacher);
    expect(chosenStudent).toEqual(spanishStudent2);
    });
    // TODO: This behavior is probably unintended, but the algorithm has not been changed.
    // It appears as though students who have waited *less* than the ELL threshold are actually
    // processed in the opposite order than we intend. Once they get beyond the ELL threshold
    // then wait time correctly influences the student scoring.

    View Slide

  203. TALK TO STAKEHOLDERS

    View Slide

  204. THEY SAID TO PUNT ON IT
    FIX EVENTUALLY
    BUT BEEN LIVING WITH IT FOR YEARS

    View Slide

  205. View Slide

  206. WE ARE A
    PITTSBURGH
    COMPANY

    View Slide

  207. SO ON THE PUNT RETURN…

    View Slide

  208. View Slide

  209. FOUND THE BUG!

    View Slide

  210. PAST-SELF FUDGED SOME
    MATH
    I WORK FOR A MATH COMPANY
    i·ro·ny
    /ˈīrənē/ noun

    View Slide

  211. FIXED THE CALCULATION
    REVERTED THE SPEC

    View Slide

  212. TESTS PASS

    View Slide

  213. HOWEVER…
    AN ACCEPTANCE TEST BROKE ELSEWHERE
    UNRELATED TO LANGUAGE

    View Slide

  214. CURSE JAVASCRIPT

    View Slide

  215. View Slide

  216. View Slide

  217. TESTS SHOULD TELL A STORY
    IN THIS ACCEPTANCE TEST

    View Slide

  218. TESTS SHOULD TELL A STORY
    IN THIS ACCEPTANCE TEST
    ▸ Had an English Speaking teacher accepting fast pass

    View Slide

  219. TESTS SHOULD TELL A STORY
    IN THIS ACCEPTANCE TEST
    ▸ Had an English Speaking teacher accepting fast pass
    ▸ Enqueued a English speaking student

    View Slide

  220. TESTS SHOULD TELL A STORY
    IN THIS ACCEPTANCE TEST
    ▸ Had an English Speaking teacher accepting fast pass
    ▸ Enqueued a English speaking student
    ▸ Waited a while

    View Slide

  221. TESTS SHOULD TELL A STORY
    IN THIS ACCEPTANCE TEST
    ▸ Had an English Speaking teacher accepting fast pass
    ▸ Enqueued a English speaking student
    ▸ Waited a while
    ▸ Enqueued another English speaking student w/ fast pass

    View Slide

  222. TESTS SHOULD TELL A STORY
    IN THIS ACCEPTANCE TEST
    ▸ Had an English Speaking teacher accepting fast pass
    ▸ Enqueued a English speaking student
    ▸ Waited a while
    ▸ Enqueued another English speaking student w/ fast pass
    ▸ Fast pass ensures second student gets max score, no way
    to lose

    View Slide

  223. WE LOST

    View Slide

  224. TEST FIXTURE WASN’T BEING
    SET CORRECTLY

    View Slide

  225. PATCHED, STILL FAILED
    MEANING THIS WAS A PRODUCTION BUG

    View Slide

  226. PATCHED CODE
    DESERIALIZATION AND CONSTRUCTION
    DIFFERED

    View Slide

  227. TESTS PASS

    View Slide

  228. TESTS SHOULD TELL A STORY
    WHAT HAPPENED?

    View Slide

  229. TESTS SHOULD TELL A STORY
    WHAT HAPPENED?
    ▸ Earlier bug hid this
    ▸ Forced shorter times to be favored on language match
    ▸ Was documented as working the other way

    View Slide

  230. TESTS SHOULD TELL A STORY
    WHAT HAPPENED?
    ▸ Earlier bug hid this
    ▸ Forced shorter times to be favored on language match
    ▸ Was documented as working the other way
    ▸ Our acceptance test worked because of order dependency
    ▸ Only two students, last student was under test and won
    favorability
    ▸ Putting another student closer could have prevented failure

    View Slide

  231. MORALS OF THE STORY

    View Slide

  232. TESTS SHOULD TELL A STORY
    MORALS OF THE STORY

    View Slide

  233. TESTS SHOULD TELL A STORY
    MORALS OF THE STORY
    ▸ Favor explicit assertions over potential coincidence

    View Slide

  234. TESTS SHOULD TELL A STORY
    MORALS OF THE STORY
    ▸ Favor explicit assertions over potential coincidence
    ▸ Favor specs that tell the story you need to know

    View Slide

  235. TESTS SHOULD TELL A STORY
    MORALS OF THE STORY
    ▸ Favor explicit assertions over potential coincidence
    ▸ Favor specs that tell the story you need to know
    ▸ Beware of coincidental truths

    View Slide

  236. TESTS SHOULD TELL A STORY
    MORALS OF THE STORY
    ▸ Favor explicit assertions over potential coincidence
    ▸ Favor specs that tell the story you need to know
    ▸ Beware of coincidental truths
    ▸ Don’t be overly clever with fixtures/fake data

    View Slide

  237. TESTS SHOULD TELL A STORY
    MORALS OF THE STORY
    ▸ Favor explicit assertions over potential coincidence
    ▸ Favor specs that tell the story you need to know
    ▸ Beware of coincidental truths
    ▸ Don’t be overly clever with fixtures/fake data
    ▸ Our careers are founded on math - learn more math

    View Slide

  238. QUESTIONS?

    View Slide

  239. I’M MATT MACHUGA
    Twitter, GitHub, Freenode: machuga

    View Slide

  240. ERIE, PA

    View Slide

  241. ERIE, PA

    View Slide

  242. Hiring Software Developers!
    TTM IS HIRING!
    CONSULTING & TEACHING
    Small to medium projects

    View Slide