In our twoprevious posts on this topic we setup Jasmine in preparation for simple browser-based tic-tac-toe game, established our requirements for the game, and wrote our first test and first bit of functionality for the game. In this post we're going to work quickly through the iterative process of test-driven development.
Before we write a bunch of tests we're going to introduce the beforeEach and afterEach functions of Jasmine. As their names describe beforeEach and afterEach are executed before and after each spec (test) in a describe block, respectively. We're going to migrate some pieces from our first test into these functions so we can reuse them.
Changing our tests to be more concise, reusable, or more efficient is part of the very important refactoring step in test-driven development.
describe('selectBox', () => { beforeEach(() => { const elementToCreate = document.createElement('button'); elementToCreate.id = 'topLeft'; elementToCreate.innerHTML = ' '; elementToCreate.onclick = selectBox(elementToCreate); document.body.appendChild(elementToCreate); }); it('should mark the box with the current players marker', () => { // arrange const element = document.getElementById('topLeft'); // act element.click(); // assert expect(element.innerHTML).toBe('X'); }); afterEach(() => { document.body.removeChild(document.getElementById('topLeft')); }); });
Now that we've refactored our unit test we can quickly write some more. We recommend writing the unit test blocks before writing all of the tests. Doing so helps us identify potential discrepancies in the requirements and gives us a better understanding of what we're being asked to code.
describe('selectBox', () => { beforeEach(() => { const elementToCreate = document.createElement('button'); elementToCreate.id = 'topLeft'; elementToCreate.innerHTML = ' '; elementToCreate.onclick = selectBox(elementToCreate); document.body.appendChild(elementToCreate); }); it('should mark the box with the current players marker', () => { // arrange const element = document.getElementById('topLeft'); // act element.click(); // assert expect(element.innerHTML).toBe('X'); }); it('should become Os turn when X marks a box', () => {}); it('should mark the box with O when the current player is X', () => {}); it('should become Xs turn when O marks a box', () => {}); it('should end the game when O marks three boxes in a row', () => {}); it('should end the game when X marks three boxes in a column', () => {}); it('should end the game when O marks three boxes diagonally', () => {}); it('should end the game when all boxes are marked and neither player has won', () => {}); afterEach(() => { document.body.removeChild(document.getElementById('topLeft')); }); });
While we were writing our tests we noticed that our requirements never specified how to start a new game, whether to keep track of how many wins each player had or how many games ended in a draw. We're going to move forward without these features, but it's good that we identified them now. If the product owner really wanted them, we would have been able to renegotiate the work we're doing to make sure we included the most important features in the first release.
Now that we have our tests started we can start writing them. For the sake of brevity, we've written them all and included them here.
describe('selectBox', () => { beforeEach(() => { const elementToCreate = document.createElement('button'); elementToCreate.id = 'topLeft'; elementToCreate.innerHTML = ' '; elementToCreate.onclick = selectBox(elementToCreate); document.body.appendChild(elementToCreate); }); it('should mark the box with X when the current player is O', () => { // arrange const element = document.getElementById('topLeft'); // act element.click(element); // assert expect(element.innerHTML).toBe('X'); }); it('should become Os turn when X marks a box', () => { // arrange player = 'X'; const element = document.getElementById('topLeft'); // act element.click(element); // assert expect(player).toBe('O'); }); it('should mark the box with O when the current player is X', () => { // arrange player = 'O'; const element = document.getElementById('topLeft'); // act element.click(element); // assert expect(element.innerHTML).toBe('O'); }); it('should become Xs turn when O marks a box', () => { // arrange player = 'O'; const element = document.getElementById('topLeft'); // act element.click(element); // assert expect(player).toBe('X'); }); it('should end the game when O marks three boxes in a row', () => { // arrange player = 'O'; moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', '']; // act element.click(element); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>'); }); it('should end the game when X marks three boxes in a column', () => { // arrange player = 'X'; moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', '']; // act element.click(element); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>'); }); it('should end the game when O marks three boxes diagonally', () => { // arrange player = 'O'; moves = ['', 'X', 'X', 'X', 'O', '', '', '', 'O']; // act element.click(element); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>'); }); it('should end the game when all boxes are marked and neither player has won', () => { // arrange player = 'X'; moves = ['', 'X', 'O', 'O', 'O', 'X', 'X', 'O', 'X']; // act element.click(element); // assert expect( document.getElementById('gameStatus') .innerHTML).toBe('<h1>Cat\'s Game!</h1>'); }); afterEach(() => { document.body.removeChild(document.getElementById('topLeft')); }); });
All of our tests fail except the first one, which is perfectly normal. Now we write just a little bit of code, using our tests as our guides.
let moves = ['', '', '', '', '', '', '', '', '']; let player = 'X'; function selectBox() { this.innerHTML = player; if (this.id === 'topLeft') { moves[0] = player; } else if (this.id === 'topMiddle') { moves[1] = player; } else if (this.id === 'topRight') { moves[2] = player; } else if (this.id === 'centerLeft') { moves[3] = player; } else if (this.id === 'centerMiddle') { moves[4] = player; } else if (this.id === 'centerRight') { moves[5] = player; } else if (this.id === 'bottomLeft') { moves[6] = player; } else if (this.id === 'bottomMiddle') { moves[7] = player; } else if (this.id === 'bottomRight') { moves[8] = player; } if (moves[0] === moves[1] && moves[1] === moves[2]) { document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[0] === moves[3] && moves[3] === moves[6]) { document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[0] === moves[4] && moves[4] === moves[8]) { document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else { document.getElementById('gameStatus').innerHTML = '<h1>Cat\'s Game!</h1>'; } player = player === 'X' ? 'O' : 'X'; }
This function causes all of our tests to pass, but doesn't mean the game will work. We're only checking whether the top row is a win, or the left column, or diagonally from top left to bottom right. If someone marks all three boxes in the middle row they won't win. We'll need to go back and write more tests, then update our code for the tests to pass.
We've added the additional tests we needed and we also refactored some of our test code to include some helper methods to create and remove all of the buttons from the form for us for each test.
describe('selectBox', () => { function createButton(id) { const elementToCreate = document.createElement('button'); elementToCreate.id = id; elementToCreate.innerHTML = ' ' elementToCreate.onclick = selectBox; document.body.appendChild(elementToCreate); } beforeEach(() => { createButton('topLeft'); createButton('topMiddle'); createButton('topRight'); createButton('centerLeft'); createButton('centerMiddle'); createButton('centerRight'); createButton('bottomLeft'); createButton('bottomMiddle'); createButton('bottomRight'); const gameStatusElement = document.createElement('div'); gameStatusElement.id = 'gameStatus'; document.body.appendChild(gameStatusElement); }); it('should mark the box with X when the current player is O', () => { // arrange const element = document.getElementById('topLeft'); // act element.click(); // assert expect(element.innerHTML).toBe('X'); }); it('should become Os turn when X marks a box', () => { // arrange player = 'X'; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(player).toBe('O'); }); it('should mark the box with O when the current player is X', () => { // arrange player = 'O'; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(element.innerHTML).toBe('O'); }); it('should become Xs turn when O marks a box', () => { // arrange player = 'O'; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(player).toBe('X'); }); it('should end the game when O marks three boxes in the top row', () => { // arrange player = 'O'; moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', '']; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>'); }); it('should end the game when X marks three boxes in the middle row', () => { // arrange player = 'X'; moves = ['', 'O', 'O', 'X', 'X', '', 'X', 'O', '']; const element = document.getElementById('centerRight'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>'); }); it('should end the game when O marks three boxes in the bottom row', () => { // arrange player = 'O'; moves = ['', '', 'X', 'X', 'X', '', 'O', '', 'O']; const element = document.getElementById('bottomMiddle'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>'); }); it('should end the game when X marks three boxes in the left column', () => { // arrange player = 'X'; moves = ['', 'O', 'O', 'X', 'X', '', 'X', '', '']; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>'); }); it('should end the game when O marks three boxes in the middle column', () => { // arrange player = 'O'; moves = ['X', 'O', '', 'X', '', 'X', '', 'O', '']; const element = document.getElementById('centerMiddle'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>'); }); it('should end the game when X marks three boxes in the right column', () => { // arrange player = 'X'; moves = ['O', 'X', 'X', 'O', '', 'X', '', 'O', '']; const element = document.getElementById('bottomRight'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>'); }); it('should end the game when O marks three boxes diagonally from top left to bottom right', () => { // arrange player = 'O'; moves = ['', 'X', 'X', 'X', 'O', '', '', '', 'O']; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>O Wins!</h1>'); }); it('should end the game when X marks three boxes diagonally from top right to bottom left', () => { // arrange player = 'X'; moves = ['O', 'O', 'X', '', 'X', '', '', 'X', 'O']; const element = document.getElementById('bottomLeft'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>X Wins!</h1>'); }); it('should not end the game when not all boxes are marked and neither player has won', () => { // arrange player = 'O'; moves = ['', 'X', 'O', '', 'O', 'X', 'X', 'O', 'X']; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe(''); }); it('should end the game when all boxes are marked and neither player has won', () => { // arrange player = 'X'; moves = ['', 'X', 'O', 'O', 'O', 'X', 'X', 'O', 'X']; const element = document.getElementById('topLeft'); // act element.click(); // assert expect(document.getElementById('gameStatus').innerHTML).toBe('<h1>Cat\'s Game!</h1>'); }); afterEach(() => { document.body.removeChild(document.getElementById('topLeft')); document.body.removeChild(document.getElementById('topMiddle')); document.body.removeChild(document.getElementById('topRight')); document.body.removeChild(document.getElementById('centerLeft')); document.body.removeChild(document.getElementById('centerMiddle')); document.body.removeChild(document.getElementById('centerRight')); document.body.removeChild(document.getElementById('bottomLeft')); document.body.removeChild(document.getElementById('bottomMiddle')); document.body.removeChild(document.getElementById('bottomRight')); document.body.removeChild(document.getElementById('gameStatus')); }); });
With these updated tests we'll need to update our code, so we've done that as well.
let moves = ['', '', '', '', '', '', '', '', '']; let player = 'X'; function selectBox() { this.innerHTML = player; if (this.id === 'topLeft') { moves[0] = player; } else if (this.id === 'topMiddle') { moves[1] = player; } else if (this.id === 'topRight') { moves[2] = player; } else if (this.id === 'centerLeft') { moves[3] = player; } else if (this.id === 'centerMiddle') { moves[4] = player; } else if (this.id === 'centerRight') { moves[5] = player; } else if (this.id === 'bottomLeft') { moves[6] = player; } else if (this.id === 'bottomMiddle') { moves[7] = player; } else if (this.id === 'bottomRight') { moves[8] = player; } if (moves[0] === player && moves[1] === player && moves[2] === player) { // top row document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[3] === player && moves[4] === player && moves[5] === player) { // middle row document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[6] === player && moves[7] === player && moves[8] === player) { // bottom row document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[0] === player && moves[3] === player && moves[6] === player) { // left column document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[1] === player && moves[4] === player && moves[7] === player) { // middle column document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[2] === player && moves[5] === player && moves[8] === player) { // right column document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[0] === player && moves[4] === player && moves[8] === player) { // top left to bottom right document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (moves[2] === player && moves[4] === player && moves[6] === player) { // top right to bottom left document.getElementById('gameStatus').innerHTML = '<h1>' + player + ' Wins!</h1>'; } else if (!!moves[0] && !!moves[1] && !!moves[2] && !!moves[3] && !!moves[4] && !!moves[5] && !!moves[6] && !!moves[7] && !!moves[8]) { document.getElementById('gameStatus').innerHTML = '<h1>Cat\'s Game!</h1>'; } player = player === 'X' ? 'O' : 'X'; }
And now our game of Tic Tac Toe should work exactly as we planned. The only part left is creating the actual markup for the game board. Since that's not really TDD we're going to show you what we used, but not go into any great detail about it. Here's our HTML file.
<html> <head> <script src="./src/tic-tac-toe.js"></script> <style> #board { height: 50%; width: 100%; position: relative; } button { font-size: 3em; width: 75px; height: 75px; position: relative; float: left; } #centerLeft, #bottomLeft, #gameStatus { clear: left; } #gameStatus { height: 10%; text-align: center; } </style> </head> <body> <div id="board"></div> <div id="gameStatus"></div> <script>createForm()</script> </body> </html>
We're creating the controls for the form dynamically so we'll show you that part, too (the createForm() function called at the bottom of the markup you see up there).
It's important to note that we skipped doing TDD on the createForm and createButton functions for this guide, but if we were really developing a game like this we absolutely would have.
let buttons = ['topLeft', 'topMiddle', 'topRight', 'centerLeft', 'centerMiddle', 'centerRight', 'bottomLeft', 'bottomMiddle', 'bottomRight']; function createButton(id) { const button = document.createElement('button'); button.innerHTML = ' '; button.onclick = selectBox; button.id = id; return button; } function createForm() { const board = document.getElementById('board'); for (let i = 0; i < buttons.length; i++) { const button = createButton(buttons[i]); board.appendChild(button); } }
These two functions are included in our tic-tac-toe.js file for easier inclusion in the page.
That's the end of the guide on using test-driven development for your JavaScript. Happy coding!