Monday, April 29, 2019

TDD in JavaScript (Part 2)

This blog post was originally published on the ThoroughTest website, back when that was a thing. As a co-founder and primary content contributor for ThoroughTest, I absolutely own the rights to this post and the source code to which it refers. I intend to reproduce each blog post here on my personal blog since the company is no longer in business.


In the previous post we setup Jasmine in preparation for a simple browser-based tic-tac-toe game. In this post we're going to actually write some tests and do some test-driven development for that game. First we need our requirements. Remember that in test-driven development our tests are written against the requirements and not the actual code. Without requirements we don't have any tests.

  1. In a game of tic-tac-toe we have a game board that has nine boxes arranged in a 3x3 pattern (3 columns and 3 rows)
  2. Our two players are Xs and Os.Xs go first.When X clicks on a box, that box is marked with an X and it becomes O's turn
  3. When O clicks on a box, that box is marked with an O and it becomes X's turn
  4. There are four possible outcomes to a game:
    • One player marks all three boxes in the same row
    • One player marks all three boxes in the same column
    • One player marks three boxes in a row diagonally
    • All boxes are selected and no player has achieved any of the other three outcomes
The rules are pretty straight forward so we should be able to generate some unit tests pretty easily from them.

A quick side not about Jasmine. Jasmine is a behavior-driven development framework, which implies a lot of things that aren't important to this post, but are important. We bring it up because our tests will be named differently than we've named them before. In Jasmine, each test is called a spec and the specs are grouped in logical blocks called describes. We recommend creating a new describe for each function you're going to test. Let's get set up to write our first spec.

I've created two new folders in tddjs: src and tests. In the tests folder, I'll create a new file called tic-tac-toe.spec.js. Because our application should be very simple we'll keep all of our specs in a single file. I'll also create a new file in the src folder called tic-tac-toe.js. Now that we have those two files (even though they're empty) we'll want to modify the SpecRunner.html file we got from Jasmine.

If you open SpecRunner.html you should see two sections marked with comments that indicate where you should include your source files and your spec files. Right now they likely reference files that we deleted earlier. We'll replace what's there with references to our two new files. SpecRunner.html now looks like this.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Jasmine Spec Runner v2.8.0</title>

  <link rel="shortcut icon"
      type="image/png" href="lib/jasmine-2.8.0/jasmine_favicon.png">
  <link rel="stylesheet" href="lib/jasmine-2.8.0/jasmine.css">

  <script src="lib/jasmine-2.8.0/jasmine.js"></script>
  <script src="lib/jasmine-2.8.0/jasmine-html.js"></script>
  <script src="lib/jasmine-2.8.0/boot.js"></script>

  <!-- include source files here... -->
  <script src="src/tic-tac-toe.js"></script>

  <!-- include spec files here... -->
  <script src="tests/tic-tac-toe.spec.js"></script>

</head>

<body>
</body>
</html>
Let's go ahead and open SpecRunner.html in our browser and take a look at what we have so far.



You should have something that looks very similar to this. We don't have any specs (tests) yet so nothing ran. That's fine. Now it's time to write our first test. Remember that in test-driven development our tests will always fail first.

We'll create a describe for a function we'll call selectBox in tic-tac-toe.spec.js.

describe('selectBox', () => {});
There's not much to see there because we still don't have our first spec written. We can add it easily enough.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {});
});
Our Jasmine tests are named a little bit more plainly than our other types of tests. The language and constructs in Jasmine are designed to read like English. That is, our test should actually be read like this: "selectBox should mark the box with the current players marker". That allows us to easily correlate our tests with our requirements and share those results with our business customers if we want. This is a feature of behavior-driven development.

If you refresh SpecRunner.html in your browser you should now see a message indicating that you have a spec, it passed, but it has no expectations. That's great! It's not actually passing, but it's not failing either. Let's change our test to make it fail.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {
 // arrange
 const element = document.getElementById('topLeft');
 element.innerHTML = ' ';
 
 // act
 element.click();
 
 // assert
 expect(element.innerHTML).toBe('X');
  });
});
This test simply tries to get an element from the page that has an id of topLeft. We make sure the element isn't already marked with anything by setting its innerHTML property to a non-breaking space. We click the element, then check to make sure its innerHTML property is now set to X. When we refresh SpecRunner.html we should now see that we have one failing test.


Jasmine tells us right away not only that we have a failing test, but also why our test is failing. In this case we can see that our test is failing because we tried to access the innerHTML property of an element that didn't exist. This is where things get a little bit more complicated. What we need to do now is create an element with the id we're looking for before our test runs.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {
    // arrange
    const elementToCreate =  document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' '
    document.body.appendChild(elementToCreate);
    const element =  document.getElementById('topLeft');
 
    // act
    element.click();
 
    // assert
    expect(element.innerHTML).toBe('X');
  });
});
With this change we're using JavaScript to programmatically add a button to our document so we can access it and click on it later. Our test still fails because clicking on our button doesn't actually do anything yet.


This time we see a couple of new things on SpecRunner.html. First, we see that the reason for the failed test has changed. We now see that the failure is caused by our expectation not being met. This is good news because it means our test is running properly, but what we expect to happen isn't happening. The second thing we see that's new is there is now a button on the document in SpecRunner.html. You can see it there in the bottom left of the screenshot.

Let's add the functionality to give our new button a click event and tell it to invoke the function selectBox when it is clicked. We'll want to pass the element being clicked into the selectBox function. We'll also add a little bit of cleanup to remove the button from the document when our test finishes. This helps keep SpecRunner clean, but it will also have greater implications as we add more tests.

describe('selectBox', () => {
  it('should mark the box with the current players marker', () => {
    // arrange
    const elementToCreate =  document.createElement('button');
    elementToCreate.id = 'topLeft';
    elementToCreate.innerHTML = ' '
    elementToCreate.onclick = selectBox(elementToCreate);
    document.body.appendChild(elementToCreate);
    const element =  document.getElementById('topLeft');
 
    // act
    element.click(element);
 
    // assert
    expect(element.innerHTML).toBe('X');

    // cleanup
    document.body.removeChild(elementToCreate);
  });
});
Our test still fails, but this time it's failing for exactly the right reason: there is no selectBox function. Also notice that the button is not showing up anymore.


Since we're going to do this iteratively (like we're supposed to) we can go ahead and add a selectBox function to our tic-tac-toe.js file.

function selectBox() { }
We're back to having a failing test because our expectation is not being met. That makes sense because nothing is happening in our actual code yet. Now we can add the code that updates the innerHTML property of the element that was clicked.

function selectBox(element) {
    element.innerHTML = 'X';
}
Our test is passing now! Astute readers will notice that our code doesn't currently correctly implement our requirements, but that's OK for right now. Right now we've written a single test that tests a single piece of code and that test passes.

We're going to take another break here. At this point we know how to setup Jasmine, write our first test, and get it to pass. In the next installment we'll add more tests and try to get closer to having a working game of Tic Tac Toe.


No comments:

Post a Comment