]

Thorne Brandt

Mobile Menu

Test Driven Development

How I Learned To Stop Worrying and Love Tests

October 1st, 2017

I was shamefully late in appreciating tests. I saw them as monotonous labor, more complex, cumbersome, and twice as time consuming as the actual code they were supposed to be testing. When I was getting started with front-end unit testing ( phantom, mocha, jasmine ), most of the time it was the tests themselves that were broken. I would spend hours stubbing methods with near-copies, which gave me the frustrating sense that I wasn't actually testing anything. Then when the code changes, you have to basically start all over again. This post is meant for anyone that is feeling similarly discouraged by the apparent lack of value compared to the effort and time-sink required for testing.

The effort is worth it. You'll start becoming a better developer the moment you become interested not if things fail, but how they fail.

One major mistake that I was making was expecting the value of the test to reveal itself directly after writing the test. You don't set up an alarm system in your house and then tap your foot waiting for a burglar to show themselves. Tests will help you sleep better at night, especially if they're still passing after a refactor.

Before most of us even hear about testing, we are trained by experience to test our surroundings and what we create with our hands and our eyes. Whether it's looking at a painting with one eye while tilting the head, or refreshing the browser as we adjust the code. This makes perfect sense when we're creating something visual. Once we add moving parts or interactivity, it becomes exponentially more time consuming and unreliable to manually test things ourselves. We could spend the rest of our lives changing small things, hitting refresh, and clicking the same buttons to verify a ux flow or application is doing what we want, and we're still going to have bugs. Or, we could develop a team of robots that, as extensions of ourselves, could silently and consistently test the farthest corners of our application, and pinpoint precise feedback about exactly where something goes wrong.

Sure, this is all fine and good. But we are still left with the dilemma of tests being annoyingly monotonous to write, right? How do we find the motivation when we just want to get the product launched? Well, what if we wrote the tests first? The tests now also serve a second function as a task manager. Turning red tests to green keeps us focused and motivated.

For an example, if we are starting a project in javascript, before we even start writing the application, we could start with one test that verifies that a named component exists. ( In this case, I will use minesweeper to demonstrate Test Driven Development, or TDD.

import MineSweeper from '../minesweeper';

describe("Minesweeper", () => {
  it('loads the minesweeper class', () => {
    let ms = new MineSweeper();
    expect(ms).toBeTruthy();
  });
});

If this was the entire app and we installed a test suite such as jest and ran 'npm test' in our app, we'd expect get something like this:




This is expected, because we have not written anything yet! To turn this green, all we have to do is create file. It can be virtually empty.

class MineSweeper{}
export default MineSweeper;

Which should turn the test green.




Now it's time to write a new test for the next process of creation. We know the app will have to manipulate the DOM, so we can just write a test for that.


import MineSweeper from '../minesweeper';

describe("Minesweeper", () => {
  let ms;
  let el;

  beforeEach(() => {
    document.body.innerHTML = '<div id="main">';
    el = document.getElementById("main");
    ms = new MineSweeper();
  });

  it('loads the minesweeper class', () => {
    let ms = new MineSweeper();
    expect(ms).toBeTruthy();
  });

  it("appends minesweeper on page", () => {
    expect(el.innerHTML).toBe("minesweeper");
  });
});

Which leads to an expected failure that should look something like the following.




Now we have a clear goal for what code we need to write to get this to pass, which is probably something like this.


class MineSweeper{
  constructor(){
    this.el = document.getElementById("main");
    this.el.innerHTML = "minesweeper";
  }
}

export default MineSweeper;

And we should get a pass.




The tests don't even need to be written to check a final product. Once we understand how test suites work, we can use them to help us learn a new language. I like to write temporary tests that I plan on replacing as soon as I know more about what the project is becoming. The 'minesweeper' innerHTML copy inside the main div in the example above is just to make sure the connection to the DOM is working, which I will promptly replace with what I actually want to be on the DOM.

I also like to make internal methods that create more testable and readable translations of more abstract methods of the application. If I'm checking something like a 2-dimensional matrix, instead of relying purely on mock data, I will create a stringify method that is meant solely for readable debugging and testing. In the spirit of TDD, before I create a tool for testing, I write a failing test for the tool itself.



...

it("Minesweeper.stringify bombs for testing", () => {
  let ms = new MineSweeper({
    rows: 3,
    cols: 3,
    bombs: 1,
    bombArray: [1]
  });
  let string = ms.stringify();
  let expectedString = "[1X1][111][000]";
  expect(string).toBe(expectedString);
});

...


If the exampleString isn't clear, the X is a bomb, and the numbers around it are how many bombs are touching that particular cell.

To get this to pass, we can write anything that will generate the expectedString from the constructor of a MineSweeper instance. In fact, we could cheat and just make a stringify method in the Minesweeper class that just returns "[1X1][111][000]". Which is why it might be helpful to make a second more complex test to make sure there's not a canned response.



...


it("Minesweeper.stringify works for 3 bombs", () => {
  let ms = new MineSweeper({
    rows: 3,
    cols: 3,
    bombs: 1,
    bombArray: [1,7]
  });
  let string = ms.stringify();
  let expectedString = "[1X1][222][1X1]";
  expect(string).toBe(expectedString);
});


...


Now we can only make this pass by writing the actual app, and a stringify method for minesweeper, ( unless we also want to write some convoluted logic to sabotage our tests, but then we're dealing with deeper moral problems of character and test coverage will be the least of our problems )

The code for the stringify method looks something like this:

class MineSweeper{

...

  stringify(){
    let stringArray = [];
    for(let row of this.cellMatrix){
      stringArray.push("[");
      for(let cell of row){
        let character;
        if(cell.isBomb){
          character = 'X'
        } else {
          character = cell.getNearbyBombCount();
        }
        stringArray.push(character);
      }
      stringArray.push("]");
    }
    return(stringArray.join(""));
  }

...

}


I will write something like this early in the development process and the stringify method won't be passing for a while, but it will help me think about what tests to write next. To follow the complete workflow for this test minesweeper exercise, follow along with my commits in this repo

The last tip I would recommend is to resist the urge to make all tests passing before you stop work. It is much more efficient to leave one test failing when you stop working for the day. It allows you, in the future, to pick up your train of thought right where you left off.

In this blog post, we've talked about why tests are important, how to motivate oneself to write tests by giving them multiple responsibilities, shared some barebones examples and tips, and linked to an example Test-Driven-Development app repo.

I'd love to hear any feedback that you have on Test Driven Development, as I've just recently started to have strong feelings about the importance of the practice.