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

Extending Your TDD Cycle Into JavaScript

Extending Your TDD Cycle Into JavaScript

How do we extend our TDD cycle into JavaScript? It isn't primarily a matter of tools, or language, but architecture. This presentation goes through some architectural principle that will make your JS easier to test, and then goes through an example of GOOS style TDD by building a Tic-Tac-Toe application.

gmoeck

May 16, 2012
Tweet

More Decks by gmoeck

Other Decks in Programming

Transcript

  1. “All Of The Pain That We Feel When Writing Unit

    Tests Points At Underlying Design Problems. Michael Feathers, The Deep Synergy Between Good Design and Testability Wednesday, May 16, 12
  2. “In Test First Development, When We Feel Pain, We Change

    Our Tests. In Test Driven Development When We Feel Pain, We Change Our Architecture Corey Haines, Fast Rails Tests Wednesday, May 16, 12
  3. Imagine A Newbie Has Heard About The “Wonders” Of TDD,

    And Comes To You With A Question: Wednesday, May 16, 12
  4. <?php ... <div id= “vault_items”> ... $query1 = "SELECT *

    FROM storage_access_vault_items WHERE access_id = {$_GET[pid]}"; $result1 = mysql_query($query1); $inner_vault_items = array(); while($this_item = mysql_fetch_assoc($result1)) { ?> <div class= “vault_item”> <?= $this_item[‘description’] ?> ... </div><?php } ... </div> ?> Wednesday, May 16, 12
  5. CONFIG = ENV['CONFIG'] || 'Debug' require 'rack' namespace :test do

    desc "Run all acceptance tests" task :acceptance do system("rspec spec/acceptance/end_to_end.rb --color") end desc "Run all unit tests" task :unit do system("node node_modules/jasmine-node/lib/jasmine-node/cli.js spec/unit --color") end desc "Run all integration tests" task :integration do File.delete("spec/integration/runner/all_tests.js") if File.file?("spec/integration/runner/all_tests.js") File.open("spec/integration/runner/all_tests.js", 'w') do |f| Dir.glob("spec/integration/*_spec.js").each do |file_name| file_name = file_name.slice(/[a-zA-z]*_spec/) f.write("require('#{file_name}');\n") end end system("./bin/build_integration_tests.js") system("open spec/integration/runner/runner.html") end task :all => [:unit, :acceptance] Wednesday, May 16, 12
  6. describe "end to end acceptance test", :type => :request do

    before(:each) do @application = TicTacToeApplicationDriver.new @application.start end it "marks the board" do @application.mark_board(1,1) @application.shows_board( [ [' ',' ',' '], [' ','X',' '], [' ',' ',' '] ] ) end end Wednesday, May 16, 12
  7. class TicTacToeApplicationDriver include Capybara::DSL include Capybara::RSpecMatchers APPLICATION_PORT = 1234 def

    initialize @application_server = ApplicationServer.new end def start @application_server.start visit "http://localhost:#{APPLICATION_PORT}/index.html" end def mark_board(x,y) cell_at(x,y).click end def shows_board(board) board.each_index do |row| board[row].each_index do |column| if board[row][column] != ' ' cell_at(column, row).text.should == board[row][column] end end end end private def cell_at(x,y) find("[data-board-x='#{x}'][data-board-y='#{y}']") end end Wednesday, May 16, 12
  8. var DOMBoardView = require('ui/dom_board_view').DOMBoardView; describe('DOMBoardView', function() { var view; beforeEach(function()

    { view = new BoardView(); view.renderBoard(3,3); }); afterEach(function() { view.remove(); }); it('renders the proper number of cells', function() { expect(document.querySelectorAll('[data-board-x][data-board- y]').length).toBe(9); }); }); spec/integration/dom_board_view_spec.js Wednesday, May 16, 12
  9. var DOMBoardView = function() { }; DOMBoardView.prototype = { renderBoard:

    function(rows, columns) { for(var i = 0; i < rows; i++) { for(var j = 0; j < columns; i++) { var cell = document.createElement('div'); cell.setAttribute('data-board-x', j); cell.setAttribute('data-board-y', i); document.body.appendChild(cell); } } } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  10. var DOMBoardView = require('ui/dom_board_view').DOMBoardView; var fireEvent = require('./test_helpers').fireEvent; describe('DOMBoardView', function()

    { ... describe('when clicking on a cell', function() { it('marks the cell with "X" when clicked', function() { fireEvent(document.querySelector('[data-board-x="1"][data- board-y="1"]'), 'click'); expect(document.querySelector('[data-board-x="1"][data- board-y="1"]').innerText).toEqual('X'); }); }); }); spec/integration/dom_board_view_spec.js Wednesday, May 16, 12
  11. var DOMBoardView = function() { }; DOMBoardView.prototype = { renderBoard:

    function(rows, columns) { for(var i = 0; i < rows; i++) { for(var j = 0; j < columns; i++) { var cell = document.createElement('div'); cell.setAttribute('data-board-x', j); cell.setAttribute('data-board-y', i); document.body.appendChild(cell); } } } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  12. DOMBoardView.prototype = { renderBoard: function(rows, columns) { for(var i =

    0; i < rows; i++) { for(var j = 0; j < columns; i++) { var cell = document.createElement('div'); cell.setAttribute('data-board-x', j); cell.setAttribute('data-board-y', i); cell.addEventListener('click', this._cellClicked.bind(this, i, j)); document.body.appendChild(cell); } } }, _cellClicked: function(row, column) { var cell = document.querySelector( '[data-board-x="' + column + '"]' + '[data-board-y="' + row + '"]'); cell.innerText = 'X'; } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  13. DOMBoardView.prototype = { renderBoard: function(rows, columns) { for(var i =

    0; i < rows; i++) { for(var j = 0; j < columns; i++) { var cell = document.createElement('div'); cell.setAttribute('data-board-x', j); cell.setAttribute('data-board-y', i); cell.addEventListener('click', this._cellClicked.bind(this, i, j)); document.body.appendChild(cell); } } }, _cellClicked: function(row, column) { var cell = document.querySelector( '[data-board-x="' + column + '"]' + '[data-board-y="' + row + '"]'); cell.innerText = 'X'; } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  14. DOMBoardView.prototype = { renderBoard: function(rows, columns) { for(var i =

    0; i < rows; i++) { for(var j = 0; j < columns; i++) { var cell = document.createElement('div'); cell.setAttribute('data-board-x', j); cell.setAttribute('data-board-y', i); cell.addEventListener('click', this._cellClicked.bind(this, i, j)); document.body.appendChild(cell); } } }, _cellClicked: function(row, column) { var cell = document.querySelector( '[data-board-x="' + column + '"]' + '[data-board-y="' + row + '"]'); cell.innerText = 'X'; } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  15. DOMBoardView.prototype = { renderBoard: function(rows, columns) { for(var i =

    0; i < rows; i++) { for(var j = 0; j < columns; i++) { var cell = document.createElement('div'); cell.setAttribute('data-board-x', j); cell.setAttribute('data-board-y', i); cell.addEventListener('click', this._cellClicked.bind(this, i, j)); document.body.appendChild(cell); } } }, _cellClicked: function(row, column) { var cell = document.querySelector( '[data-board-x="' + column + '"]' + '[data-board-y="' + row + '"]'); cell.innerText = 'X'; } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  16. describe('DOMBoardView', function() { ... describe('when clicking on a cell', function()

    { it('marks the cell with "X" when clicked', function() { var listener = { cellSelected: jasmine.createSpy('listener#cellSelected') }; view.addListener('cellSelected', listener); fireEvent(document.querySelector('[data-board-x="1"][data- board-y="1"]'), 'click'); expect(listener.cellSelected).toHaveBeenCalledWith({ row: 1, column: 1 }); }); }); }); spec/integration/dom_board_view_spec.js Wednesday, May 16, 12
  17. DOMBoardView.prototype = { ... addListener: function(event, listener) { this._listener =

    listener; }, _cellClicked: function(row, column) { this._listener.cellSelected(row, column); } }; src/ui/dom_board_view.js Wednesday, May 16, 12
  18. var DOMBoardView = require('./ui/dom_board_view').DOMBoardView; document.addEventListener("DOMContentLoaded", function() { var boardView =

    new DOMBoardView(); var eventHandler = { cellSelected: function(row, column) { boardView.markCell(row, column, ‘X’); } }; boardView.addListener(‘cellSelected’, eventHandler); boardView.renderBoard(3,3); }); main.js Wednesday, May 16, 12
  19. var DOMBoardView = require('./ui/dom_board_view').DOMBoardView; document.addEventListener("DOMContentLoaded", function() { var boardView =

    new DOMBoardView(); var eventHandler = { cellSelected: function(row, column) { boardView.markCell(row, column, ‘X’); } }; boardView.addListener(‘cellSelected’, eventHandler); boardView.renderBoard(3,3); }); main.js Wednesday, May 16, 12
  20. var DOMBoardView = require('./ui/dom_board_view').DOMBoardView; var TurnTracker = require('./turn_tracker').TurnTracker; document.addEventListener("DOMContentLoaded", function()

    { var boardView = new DOMBoardView(); var turnTracker = new TurnTracker(‘X’, ‘O’); var eventHandler = { cellSelected: function(row, column) { boardView.markCell(row, column, ‘X’); turnTracker.playerOwnsNewCell({row: row, column: column}); } }; turnTracker.addListener(‘newPlayersTurn’, eventHandler); boardView.addListener(‘cellSelected’, eventHandler); turnTracker.startNewGame(); boardView.renderBoard(3,3); }); main.js Wednesday, May 16, 12
  21. var TurnTracker = require('../../src/turn_tracker').TurnTracker; describe('TurnTracker', function() { it('notifies its listeners

    that it is the first players turn when told to start a new game', function() { var player1 = {player: 1}, player2 = {player:2}; var turnTracker = new TurnTracker(player1, player2); var listener = { newPlayersTurn: jasmine.createSpy(‘listener#newPlayersTurn’) }; turnTracker.addListener(‘newPlayersTurn’, listener); turnTracker.startNewGame(); expect(listener.newPlayersTurn).toHaveBeenCalledWith( player1); }); }); spec/unit/turn_tracker_spec.js Wednesday, May 16, 12
  22. var TurnTracker = function(player1, player2) { this._player1 = player1; this._player2

    = player2; }; TurnTracker.prototype = { addListener: function(event, listener) { this._listener = listener; }, startNewGame: function() { this._listener.newPlayersTurn(player1); } }; src/turn_tracker.js Wednesday, May 16, 12
  23. var Announce = require('../../src/util/announcer').Announcer; describe('Announcer', function() { var announcer, listener;

    beforeEach(function() { announcer = new Announcer(); listener = { someEvent: jasmine.createSpy('listener#someEvent') }; }); it('notifies its listeners when an event they are registered for happens', function() { announcer.addListener('someEvent', listener); announcer.announce('someEvent', 'abc'); expect(listener.someEvent).toHaveBeenCalledWith('abc'); }); }); spec/unit/announcer_spec.js Wednesday, May 16, 12
  24. var Announcer = function() { }; Announcer.prototype = { addListener:

    function(event, listener) { this._listener = listener; }, announce: function(event, data) { this._listener.someEvent(data); } }; src/util/announcer.js Wednesday, May 16, 12
  25. var Announce = require('../../src/util/announcer').Announcer; describe('Announcer', function() { ... it('does not

    notify its listeners when an event they are not registered for happens', function() { announcer.addListener('anotherEvent', listener); announcer.announce('someEvent', 'abc'); expect(listener.someEvent).not.toHaveBeenCalled(); }); }); spec/unit/announcer_spec.js Wednesday, May 16, 12
  26. var Announcer = function() { }; Announcer.prototype = { addListener:

    function(event, listener) { this._listener = listener; this._event = event; }, announce: function(event, data) { if (this._event === event) { this._listener.someEvent(data); } } }; src/util/announcer.js Wednesday, May 16, 12
  27. var Announce = require('../../src/util/announcer').Announcer; describe('Announcer', function() { ... it('notifies multiple

    listeners when an event they are registered for happens', function() { var listener2 = { anotherEvent: jasmine.createSpy('listener2#anotherEvent') }; announcer.addListener('anotherEvent', listener); announcer.addListener('anotherEvent', listener2); announcer.announce('anotherEvent', 'abc'); expect(listener.someEvent).toHaveBeenCalledWith('abc'); expect(listener2.someEvent).toHaveBeenCalledWith('abc'); }); }); spec/unit/announcer_spec.js Wednesday, May 16, 12
  28. var Announcer = function() { this._events = {}; }; Announcer.prototype

    = { addListener: function(event, listener) { this._events[event] = this._events[event] || []; this._events[event].push(listener); }, announce: function(event, data) { var registeredListeners = this._events[event] || []; registeredListeners.forEach(function(listener) { listener[event](data); }); } }; src/util/announcer.js Wednesday, May 16, 12
  29. var Announcer = require('./util/announcer').Announcer; var TurnTracker = function(player1, player2) {

    this._player1 = player1; this._player2 = player2; this._announcer = new Announcer(); }; TurnTracker.prototype = { addListener: function(event, listener) { this._announcer.addListener(event, listener); }, startNewGame: function() { this._announcer.announce('newPlayersTurn', this._player1); } }; src/turn_tracker.js Wednesday, May 16, 12
  30. var TurnTracker = require('../../src/turn_tracker').TurnTracker; describe('TurnTracker', function() { ... it('notifies its

    listeners that it is the next players turn when told to that a player now owns a new cell', function() { turnTracker.startNewGame(); listener.newPlayersTurn.reset(); turnTracker.playerOwnsNewCell({row: 1, column: 1}); expect(listener.newPlayersTurn).toHaveBeenCalledWith( player2); }); }); spec/unit/turn_tracker_spec.js Wednesday, May 16, 12
  31. var STATES = { PLAYER1_TURN: { playerOwnsNewCell: function(newCellInformation) { this._announcer.announce('newPlayersTurn',

    this._player2); this._currentState = STATES.PLAYER2_TURN; } }, PLAYER2_TURN: { playerOwnsNewCell: function(newCellInformation) { this._announcer.announce('newPlayersTurn', this._player1); this._currentState = STATES.PLAYER1_TURN; } } }; var TurnTracker = function(player1, player2) { this._announcer = new Announcer(); this._player1 = player1; this._player2 = player2; this._currentState = STATES.PLAYER1_TURN; }; TurnTracker.prototype = { addListener: function(event, listener) { this._announcer.addListener(event, listener); }, playerOwnsNewCell: function(newCellInformation) { this._currentState.playerOwnsNewCell.call(this, newCellInformation); }, startNewGame: function() { this._announcer.announce('newPlayersTurn', this._player1); this._currentState = STATES.PLAYER1_TURN; } }; src/turn_tracker.js Wednesday, May 16, 12
  32. describe "end to end acceptance test", :type => :request do

    ... it "marks the board" do ... @application.mark_board(1,0) @application.shows_board( [ ['X','X',' '], ['O','O',' '], [' ',' ',' '] ] ) @application.mark_board(0,2) @application.shows_board( [ ['X','X','X'], ['O','O',' '], [' ',' ',' '] ] ) @application.shows_player1_won end end Wednesday, May 16, 12
  33. DOM Board View Main cellSelected DOMBoardViewListener markCell Turn Tracker newPlayersTurn

    TurnTrackerListener playerOwnsNewCell Wednesday, May 16, 12
  34. DOM Board View Main Turn Tracker Player playerOwnsNewCell markCell DOM

    Alert Renderer showPlayerWonGame receiveCell Wednesday, May 16, 12
  35. document.addEventListener("DOMContentLoaded", function() { var boardView = new DOMBoardView(); var player1

    = new Player('X', boardView, alertRenderer); var player2 = new Player('O', boardView, alertRenderer); var turnTracker = new TurnTracker(player1, player2); var eventHandler = { cellSelected: function(cellInformation) { this._currentPlayer.receiveCell(cellInformation); }, newPlayersTurn: function(player) { this._currentPlayer = player; } } var alertRenderer = new DOMAlertRenderer(); turnTracker.addListener('newPlayersTurn', eventHandler); boardView.addListener('cellSelected', eventHandler); player1.addListener('playerOwnsNewCell', turnTracker); player2.addListener('playerOwnsNewCell', turnTracker); turnTracker.startNewGame(); boardView.renderBoard(3, 3); }); main.js Wednesday, May 16, 12
  36. describe('Player', function() { var player, board, listener; beforeEach(function() { board

    = new BoardRole(); player = new Player('X', board); listener = new PlayerListenerRole(); player.addListener('playerOwnsNewCell', listener); }); describe('when it is told that to receive a cell', function() { beforeEach(function() { player.receiveCell({row: 2, column: 2}); }); it('tells its board to mark that square with its marker', function() { expect(board.markCell).toHaveBeenCalledWith(2,2,'X'); }); }); spec/unit/player_spec.js Wednesday, May 16, 12
  37. var Player = function(marker, board, alertRenderer) { this._marker = marker;

    this._board = board; }; Player.prototype = { receiveCell: function(cellInformation) { this._board.markCell(cellInformation.row, cellInformation.column, this._marker); } }; src/player.js Wednesday, May 16, 12
  38. describe('Player', function() { var player, board, listener; beforeEach(function() { board

    = new BoardRole(); player = new Player('X', board); listener = new PlayerListenerRole(); player.addListener('playerOwnsNewCell', listener); }); describe('when it is told that to receive a cell', function() { beforeEach(function() { player.receiveCell({row: 2, column: 2}); }); it('tells its board to mark that square with its marker', function() { expect(board.markCell).toHaveBeenCalledWith(2,2,'X'); }); it('notifies its listeners that it owns the cell that was selected', function() { expect(listener.playerOwnsNewCell).toHaveBeenCalledWith( {row: 2, column: 2}); }); ... }); spec/unit/player_spec.js Wednesday, May 16, 12
  39. var Player = function(marker, board, alertRenderer) { this._marker = marker;

    this._board = board; this._announcer = new Announcer(); }; Player.prototype = { receiveCell: function(cellInformation) { this._board.markCell(cellInformation.row, cellInformation.column, this._marker); this._announcer.announce('playerOwnsNewCell’, cellInformation); } }; src/player.js Wednesday, May 16, 12
  40. describe('Player', function() { ... it('notifies tells its alerter that it

    has one the game when it receives all the cells in a row', function() { player.receiveCell({row: 0, column: 0}); player.receiveCell({row: 0, column: 1}); player.receiveCell({row: 0, column: 2}); expect(alerter.playerWonGame).toHaveBeenCalledWith({player: 'X'}); }); }); spec/unit/player_spec.js Wednesday, May 16, 12
  41. var Player = function(marker, board, alertRenderer) { this._marker = marker;

    this._board = board; this._alerter = alertRenderer; this._announcer = new Announcer(); this._rows = []; for(var i = 0; i < 3; i++) { this._rows[i] = new CellTracker(3); } }; Player.prototype = { receiveCell: function(cellInformation) { this._board.markCell(cellInformation.row, cellInformation.column, this._marker); this._announcer.announce('playerOwnsNewCell’, cellInformation); this._rows[cellInformation.row].takeCell(); if (this._hasWonGame(cellInformation)) { this._alerter.playerWonGame({player: this._marker}); } } }; ... src/player.js Wednesday, May 16, 12
  42. var CellTracker = function(totalCells) { this._remainingCells = totalCells; }; CellTracker.prototype

    = { takeCell: function() { this._remainingCells -= 1; }, hasAllCells: function() { return this._remainingCells === 0; } }; src/player.js Wednesday, May 16, 12
  43. var DOMAlertRenderer = require('ui/dom_alert_renderer').DOMAlertRenderer; describe('DOMAlertRenderer', function() { it('renders an alert

    when a player wins the game', function() { var renderer = new DOMAlertRenderer(); renderer.playerWonGame({player: 'X'}); expect(document.body.querySelector('.alert').innerText).toEqual("'X' Wins"); }); }); spec/integration/dom_alert_renderer_spec.js Wednesday, May 16, 12
  44. var DOMAlertRenderer = function() { }; DOMAlertRenderer.prototype = { playerWonGame:

    function(playerInformation) { var element = document.createElement('div'); element.className = 'alert'; element.innerText = ”’” + playerInformation.player + “‘ Wins”; document.body.appendChild(element); } }; ui/dom_alert_renderer.js Wednesday, May 16, 12
  45. DOM Board View Main Turn Tracker Player playerOwnsNewCell markCell DOM

    Alert Renderer showPlayerWonGame receiveCell Wednesday, May 16, 12
  46. DOM Board View Main Turn Tracker Player playerOwnsNewCell markCell DOM

    Alert Renderer showPlayerWonGame receiveCell Wednesday, May 16, 12