Save 37% off PRO during our Black Friday Sale! »

Tests Should Tell a Story

Tests Should Tell a Story

Given at Laracon US 2016.

8f9f6a577da77a9add9cadbb90e66b75?s=128

Matthew Machuga

July 28, 2016
Tweet

Transcript

  1. TESTS SHOULD TELL A STORY LARACON US 2016

  2. STORIES

  3. WHO KNOWS THIS MAN?

  4. FRED ROGERS

  5. THE NEIGHBORHOOD LIVES ON

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

  8. None
  9. None
  10. None
  11. None
  12. YOUR CODE TELLS A STORY

  13. None
  14. TESTS SHOULD TELL A STORY DISTILLED SHARED KNOWLEDGE ▸ Good

    naming ▸ Good abstractions ▸ Details pushed down ▸ Intent clearly expressed ▸ Test driven (mostly)
  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)
  16. STORIES ARE NOT JUST TOLD THEY ARE INTERPRETED

  17. I NEED MAYO Coheed and Cambria HEADSHOT KID DISASTER FROM

    SECOND STAGE TURBINE BLADE
  18. WHAT DO YOU SEE?

  19. None
  20. EVEN FUTURE-YOU WILL INTERPRET CODE DIFFERENTLY

  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
  22. None
  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
  24. AMBIGUITY LEAVES ROOM FOR INTERPRETATION

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

  26. WE ARE NOT BOB ROSS

  27. WE MAKE MISTAKES

  28. OUR MISTAKES HAVE CONSEQUENCES

  29. WRITE TESTS TO PREVENT HAPPY ACCIDENTS

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

  31. ಠ_ಠ

  32. ಠ_ಠ

  33. ಠ_ಠ

  34. None
  35. KEYNOTE !"

  36. None
  37. TESTS

  38. ANATOMY OF A TEST

  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
  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
  41. TESTS SHOULD TELL A STORY ANATOMY OF A TEST -

    GIVEN-WHEN-THEN ▸ Arrange - Given ▸ Act - When ▸ Assert - Then
  42. SETTING A BASELINE

  43. TESTS SHOULD TELL A STORY PHPUNIT <?php 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); } }
  44. XUNIT

  45. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  46. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  47. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  48. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  49. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  50. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  51. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  52. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  53. SPEC/BDD STYLE

  54. AT A BASIC LEVEL VERY SIMILAR

  55. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  56. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  57. TESTS SHOULD TELL A STORY XUNIT - PHPUNIT <?php 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); } }
  58. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  59. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  60. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  61. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  62. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  63. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  64. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  65. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  66. TESTS SHOULD TELL A STORY SPEC/BDD - PHO <?php 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); }); });
  67. TESTS SHOULD TELL A STORY NESTED CONTEXTS <?php 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); }); }); });
  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
  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
  70. HOW ARE YOU FEELING ABOUT THE SPEC DSL?

  71. HOW ARE YOU FEELING ABOUT THE SPEC DSL?

  72. SPEC LIBRARIES WE’LL BE USING

  73. SPEC LIBRARIES WE’LL BE USING

  74. JASMINE & RSPEC

  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); }); });
  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
  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 ->
  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
  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
  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
  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
  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
  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
  84. 5 TIPS FOR SPEC STORYTELLING OR: I TRIED THESE 5

    WEIRD TRICKS ON MY TESTS; YOU’LL NEVER GUESS WHAT HAPPENED NEXT!
  85. 1. BE EXPRESSIVE

  86. CUSTOM ACTIONS FOR COMPLEX BEHAVIOR

  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
  88. TESTS SHOULD TELL A STORY LAYOUT CHECK NEXT

  89. TESTS SHOULD TELL A STORY LAYOUT CHECK NEXT

  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
  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
  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
  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
  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
  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
  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
  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
  98. TESTS ARE CODE REFACTOR!

  99. USE THE BEST MATCHER / ASSERTION

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

  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 <top (required)>' 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
  102. expect((2+2).to_s).to eq 4

  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 <top (required)>' 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
  104. CUSTOM MATCHERS / ASSERTIONS

  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; }
  106. TESTS SHOULD TELL A STORY PHPUNIT CUSTOM ASSERTIONS BLESSED METHOD

    <?php 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'; } }
  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
  108. 2. RESPECT THE CUKES

  109. TESTS SHOULD TELL A STORY CUCUMBER

  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
  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
  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
  113. 3. BAD STORIES ARE SIGNALS

  114. TESTS SHOULD TELL A STORY SMELLS OF BAD STORIES ▸

    Plot holes ▸ Lack of Character Development ▸ No Descriptions ▸ Switching narrative
  115. TESTS SHOULD TELL A STORY PLOT HOLES ▸ Mocking the

    object under test ▸ Global state mutations out of view ▸ Implicit Behavior
  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
  117. TESTS SHOULD TELL A STORY LACK OF CHARACTER DEVELOPMENT ▸

    Performing transformations on characters out of view ▸ Mocking collaborators without clear reason
  118. TESTS SHOULD TELL A STORY SWITCHING NARRATIVES ▸ Setting expectations

    on collaborators ▸ Blurring lines between suites
  119. 4. UNIT TEST IN ISOLATION

  120. MORE ON THIS SOON

  121. 5. TRY TDD/BDD

  122. TWO WEEKS

  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
  124. TESTS SHOULD TELL A STORY DRIVING YOUR DESIGN & CODE

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

    ▸ Write failing test first ▸ Evidence feature is incomplete ▸ Evidence feature is not coincidently passing
  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
  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
  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
  129. TIME FOR A STORY WARNING: MATH AHEAD

  130. None
  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
  132. None
  133. TESTS SHOULD TELL A STORY LIVE HELP ▸ Teachers available

    to help students ▸ Voice ▸ Text ▸ Whiteboard
  134. None
  135. None
  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
  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
  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
  139. THE PAIRING ALGORITHM

  140. None
  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
  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
  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
  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
  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
  146. REFACTORING SPECS

  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'); }); }); }); });
  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'); }); });
  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'); }); });
  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'); }); });
  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?
  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'); }); });
  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'); }); });
  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?
  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
  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'); }); });
  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?
  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'); }); });
  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
  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
  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
  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
  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
  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
  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
  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
  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
  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
  169. TESTS SHOULD TELL A STORY GOOD THINGS ABOUT THIS SPEC

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

    ▸ Mostly clear name for example group and test description
  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)
  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
  173. TESTS SHOULD TELL A STORY NOT-SO-GOOD THINGS ABOUT THIS SPEC

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

    ▸ Helpers cause side-effects (offsetTimes, removeStudent)
  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
  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
  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
  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
  179. TESTS PASS SPANISH STUDENTS ARE PULLED FIRST

  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
  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); }); }); });
  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
  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
  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
  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
  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
  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
  188. TESTS FAIL

  189. TESTS FAIL Expected: spanishStudent2 Actual: spanishStudent1

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

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

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

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

    ASSERTIONS
  195. A WILD BUG APPEARS MATH TO THE RESCUE!

  196. TESTS SHOULD TELL A STORY I AM AWFUL AT SPREADSHEETS

    Score w/ Language Language Weight Normal Score X-axis: Time Y-axis: Score
  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
  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
  199. WHAT DID WE DO ABOUT IT?

  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); });
  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); });
  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.
  203. TALK TO STAKEHOLDERS

  204. THEY SAID TO PUNT ON IT FIX EVENTUALLY BUT BEEN

    LIVING WITH IT FOR YEARS
  205. None
  206. WE ARE A PITTSBURGH COMPANY

  207. SO ON THE PUNT RETURN…

  208. None
  209. FOUND THE BUG!

  210. PAST-SELF FUDGED SOME MATH I WORK FOR A MATH COMPANY

    i·ro·ny /ˈīrənē/ noun
  211. FIXED THE CALCULATION REVERTED THE SPEC

  212. TESTS PASS

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

  214. CURSE JAVASCRIPT

  215. None
  216. None
  217. TESTS SHOULD TELL A STORY IN THIS ACCEPTANCE TEST

  218. TESTS SHOULD TELL A STORY IN THIS ACCEPTANCE TEST ▸

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

    Had an English Speaking teacher accepting fast pass ▸ Enqueued a English speaking student
  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
  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
  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
  223. WE LOST

  224. TEST FIXTURE WASN’T BEING SET CORRECTLY

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

  226. PATCHED CODE DESERIALIZATION AND CONSTRUCTION DIFFERED

  227. TESTS PASS

  228. TESTS SHOULD TELL A STORY WHAT HAPPENED?

  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
  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
  231. MORALS OF THE STORY

  232. TESTS SHOULD TELL A STORY MORALS OF THE STORY

  233. TESTS SHOULD TELL A STORY MORALS OF THE STORY ▸

    Favor explicit assertions over potential coincidence
  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
  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
  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
  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
  238. QUESTIONS?

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

  240. ERIE, PA

  241. ERIE, PA

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

    to medium projects