Test Driven Development

Learning Objectives

Pre-requisites

Lesson

How do we know that our code is working the way we think it is? Surely we can just run the code and see if it is working. Isn't this just testing the code?

As the programs we write get larger and more complex, it is not always possible to test our code by just running it. For example, we may want to force a particular kind of error to ensure we are handling it correctly.

This is why we write tests. Tests ensure that our software is working as we have designed. We are trying to prevent 3 things:

Errors are mistakes made in code.

Defects mean our code works but not as we intended. Defects are often called 'bugs'. This term was coined because before the use of electronic circuits, early computer programs would sometimes malfunction due to actual insects getting into the physical valves of the computer, preventing the program from running correctly.

Failures are when our programme 'crashes' i.e. stops running and errors in a way that cannot be recovered from.

Unit test frameworks

Let's look at how we create a test for the Bag class we created in the previous lesson.

describe('Bag', function () {
    test('has a weight', function () {
        const bag = new Bag(13);
        expect(bag.weight).toBe(13);
    });

    test('does not have a weight', function () {
        expect(() => new Bag()).toThrowError('bag must have a weight');
    });
})
describe('Bag', function () {
    test('has a weight', function () {
        const bag = new Bag(13);
        expect(bag.weight).toBe(13);
    });

    test('does not have a weight', function () {
        // notice how we have to run a function inside `expect` to trigger the error and catch it
        expect(() => new Bag()).toThrowError('bag must have a weight');
    });
})
public class BagTest {

    @Test
    public void hasValidWeight() throws Exception {
        final Bag bag = new Bag(13);
        assertEquals(13, bag.getWeight());
    }

    @Test(expected = Exception.class) // asserts we have an exception thrown
    public void hasInvalidWeight() throws Exception {
        final Bag bag = new Bag(0);
    }
}

Test coverage

It's really important to test all the possible branches through your code. Your company will have coding standards which specify what % of code coverage is required.

Research how to generate a coverage report for your language's test framework. Here is an example using JavaScript Jest:

Add the following line to your package.json

{
  "scripts": {
    "test": "jest --watchAll",
    "test:report": "jest --coverage=true"
  },
  "dependencies": {
    "jest": "^26.4.2"
  }
}

Then run npm run test:report - you are aiming for 100% test coverage.

You should see that Jest generates a 'coverage' report in your project folder under /coverage/Icov-report/index.html. Open this in your browser to view coverage by line, branch, function and statement.

test coverage report showing statements, branches, functions and line coverage

The report is interactive hence you can click and drill down into a specific class.

Test Driven Development (TDD)

Test Driven Development requires us to write tests first. The 'red, green, refactor' approach helps developers focus on three phases:

Assignment

  1. Use TDD to write tests for the airport classes you created in the previous lesson
  2. Commit your code into Github and share the link with your coach for review.
  3. Include a README.md file in your Github project that explains the purpose of a unit test.

Assignment extension tasks

Additional resources