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
TESTS SHOULD TELL A STORY DISTILLED SHARED KNOWLEDGE ▸ Good naming ▸ Good abstractions ▸ Details pushed down ▸ Intent clearly expressed ▸ Test driven (mostly)
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)
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
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
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
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
TESTS SHOULD TELL A STORY PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
TESTS SHOULD TELL A STORY XUNIT - PHPUNIT class UserRepositoryTest extends PHPUnit_Framework_TestCase { public function setup() { $this->subject = new UserRepository(); $this->name = ‘Machuga’; $this->subject->save(new User($this->name)); } public function teardown() { $this->subject->clear(); } public function testCanFindAUserByName() { $user = $this->subject->findByName($this->name); $this->assertEquals($this->name, $user->name); } }
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
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
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); }); });
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
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 ->
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
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
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
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
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
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
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
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
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
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
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
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
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
Failures: 1) Hi, Laracon! # Failure/Error: expect((2+2).to_s == 4).to eq true expected true got false (compared using ==) # ./testing_spec.rb:3:in `block (2 levels) in ' Finished in 0.02861 seconds (files took 0.16021 seconds to load) 1 example, 1 failure Failed examples: rspec ./testing_spec.rb:2 # My awesome example group works
Failures: 1) Hi, Laracon! # Failure/Error: expect((2+2).to_s).to eq 4 expected: 4 got: "4" (compared using ==) # ./testing_spec.rb:3:in `block (2 levels) in ' Finished in 0.01195 seconds (files took 0.13384 seconds to load) 1 example, 1 failure Failed examples: rspec ./testing_spec.rb:2 # My awesome example group works
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; }
TESTS SHOULD TELL A STORY PHPUNIT CUSTOM ASSERTIONS BLESSED METHOD use PHPUnit\Framework\TestCase; abstract class PHPUnit_Framework_Assert { public static function assertTrue($condition, $message = ‘') { self::assertThat($condition, self::isTrue(), $message); } public static function isTrue() { return new PHPUnit_Framework_Constraint_IsTrue; } } class PHPUnit_Framework_Constraint_IsTrue extends PHPUnit_Framework_Constraint { public function matches($other) { return $other === true; } public function toString() { return 'is true'; } }
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
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
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
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
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
TESTS SHOULD TELL A STORY LACK OF CHARACTER DEVELOPMENT ▸ Performing transformations on characters out of view ▸ Mocking collaborators without clear reason
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
TESTS SHOULD TELL A STORY DRIVING YOUR DESIGN & CODE ▸ Write failing test first ▸ Evidence feature is incomplete ▸ Evidence feature is not coincidently passing
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
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
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
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
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
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
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
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
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
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
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
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?
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
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
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
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
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); }); }); });
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
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
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
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
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
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
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
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
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); });
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); });
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.
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
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
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
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
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
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
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
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
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