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.

Matt Machuga

July 28, 2016
Tweet

More Decks by Matt Machuga

Other Decks in Programming

Transcript

  1. 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
  2. TESTS SHOULD TELL A STORY DISTILLED SHARED KNOWLEDGE ▸ Good

    naming ▸ Good abstractions ▸ Details pushed down ▸ Intent clearly expressed ▸ Test driven (mostly)
  3. 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)
  4. 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
  5. 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
  6. 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
  7. 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
  8. TESTS SHOULD TELL A STORY ANATOMY OF A TEST -

    GIVEN-WHEN-THEN ▸ Arrange - Given ▸ Act - When ▸ Assert - Then
  9. 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); } }
  10. 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); } }
  11. 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); } }
  12. 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); } }
  13. 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); } }
  14. 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); } }
  15. 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); } }
  16. 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); } }
  17. 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); } }
  18. 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); } }
  19. 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); }); });
  20. 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); } }
  21. 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); }); });
  22. 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); }); });
  23. 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); }); });
  24. 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); }); });
  25. 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); }); });
  26. 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); }); });
  27. 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); }); });
  28. 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); }); });
  29. 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); }); });
  30. 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); }); }); });
  31. 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
  32. 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
  33. 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); }); });
  34. 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
  35. 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 ->
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 5 TIPS FOR SPEC STORYTELLING OR: I TRIED THESE 5

    WEIRD TRICKS ON MY TESTS; YOU’LL NEVER GUESS WHAT HAPPENED NEXT!
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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; }
  55. 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'; } }
  56. 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
  57. 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
  58. 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
  59. 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
  60. TESTS SHOULD TELL A STORY SMELLS OF BAD STORIES ▸

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

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

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

    on collaborators ▸ Blurring lines between suites
  65. 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
  66. TESTS SHOULD TELL A STORY DRIVING YOUR DESIGN & CODE

    ▸ Write failing test first ▸ Evidence feature is incomplete ▸ Evidence feature is not coincidently passing
  67. 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
  68. 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
  69. 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
  70. 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
  71. TESTS SHOULD TELL A STORY LIVE HELP ▸ Teachers available

    to help students ▸ Voice ▸ Text ▸ Whiteboard
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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'); }); }); }); });
  81. 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'); }); });
  82. 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'); }); });
  83. 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'); }); });
  84. 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?
  85. 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'); }); });
  86. 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'); }); });
  87. 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?
  88. 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
  89. 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'); }); });
  90. 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?
  91. 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'); }); });
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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
  97. 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
  98. 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
  99. 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
  100. 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
  101. 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
  102. TESTS SHOULD TELL A STORY GOOD THINGS ABOUT THIS SPEC

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

    ▸ Helpers cause side-effects (offsetTimes, removeStudent)
  106. 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
  107. 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
  108. 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
  109. 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
  110. 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
  111. 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); }); }); });
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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
  117. 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
  118. TESTS SHOULD TELL A STORY I AM AWFUL AT SPREADSHEETS

    Score w/ Language Language Weight Normal Score X-axis: Time Y-axis: Score
  119. 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
  120. 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
  121. 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); });
  122. 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); });
  123. 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.
  124. TESTS SHOULD TELL A STORY IN THIS ACCEPTANCE TEST ▸

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

    Had an English Speaking teacher accepting fast pass ▸ Enqueued a English speaking student
  126. 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
  127. 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
  128. 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
  129. 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
  130. 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
  131. TESTS SHOULD TELL A STORY MORALS OF THE STORY ▸

    Favor explicit assertions over potential coincidence
  132. 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
  133. 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
  134. 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
  135. 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