Skip to content

Test Driven Development fundamentals wtih Javascript and Jest

Notifications You must be signed in to change notification settings

roppa/tdd-fundamentals

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Javascript Test Driven Development for the real world

Pre-requisites: intermediate level of javascript and npm

Goal: to create a working example of 'The Game of Life' by using Test Driven Techniques

Purpose: to get a working understanding of tests, different types of tests, and test driven development

Tools: Node.js (> v8.4.0), an IDE such as VS Code, a modern Browser such as Chrome, Firefox, later version of IE

Definitions

Assertion
to state something confidently and forcefully. This means stating an 'is-ness'; 'This is WRONG'. This then equates to a boolean, is it true or false? true would mean a passing test
Interface
a part of software or hardware that facilitates communication
Unit test
focus on one part of the software. A unit test can be run in isolation - it must not rely on the state of any previous tests
Regression testing
running existing tests to ensure that software is working after a change
Integration test
modules, functions etc tested as a group
Stub
a common test helper (could be a database connector, api endpoint) providing hard coded answers
Spy
similar to stub, they record information they were called with
Mock
mimic exactly the object they are replacing, returning specific responses. More than just a stub
Performance test/Load test
measuring how well code copes with stress
Code coverage
measures how many times a line, statement, branch (if/else) or function etc is executed
E2E (end to end) testing
testing a user journey through software from start to achieving the journeys goal

Why Test Driven Development? Most companies seek people with TDD skills, why is this? TDD does not mean 'your code has tests'. It doesn't really mean you have 'experience of using X testing library'. It means you actually write your tests first and write code to pass those test systematically.

What teams actually want from a prospective engineer is:

  • ability solve a problem in a systematic way
  • produce clean code
  • produce code which communicates to the next developer (which could also be you) exactly what the problem was

The TDD technique helps to achieve all of the above.

Technique

Test Driven Development is a technique. A tool is something that makes your life easier. A technique is how to best use the tool. The tool is the assertion library and testing framework.

Rules for test driven development are:

  • write a failing test
  • write code to pass the test
  • refactor (remove duplication)

This is the base for the three laws of TDD:

  1. You are not allowed to write production code unless it is to make a failing unit test pass
  2. You are not allowed to write any more of a unit test than is sufficient to fail; not compiling is failing
  3. You are not allowed to write more production code than is sufficient to pass the one failing unit test

Why fail first?

  • to see regression tests fail (no 0 positives)
  • it makes you think about your code
  • your code will be testable (you don't want to write code then have to refactor in order to be able to test)

There is another part to this technique. Uncle Bob put it best 'as the tests get more specific, the production code gets more generic'. This ties in with the refactoring part of TDD as we'll see later.

Application of TDD

First we have a goal 'build an x' - in our case 'build a game of life'. The goal is the end result; the product. This would then be broken down into individual tasks (usually when you work for a company you may only get individual tasks such as 'As an x I want to y so that I can z'). After selecting a task to work on you ask yourself what set of tests would confidently demonstrate that this task is complete and works as expected. This process could be:

  • break down the task and list the tests we need for it to be considered 'done'
  • 'start small or not at all' as Kent Beck says - so write the simplest test and code to pass
  • stub any dependencies
  • incrementally change constants to variables (from specific to generic)

Testing frameworks

There are many testing frameworks out there but they all consist of some or all of these tools:

  • testing harness
  • assertion library
  • reporting
  • code coverage

We will use Jest as our library because it covers everything above and more including: mocks, spies, good documentation - and we can test both front and back end code.

Our task

Our task is to implement the Conway's Game of Life using TDD. First we will build the engine, and then we will build a front end solution. We're not going to get into functional programming, Object Oriented programming etc, for this we'll just be concentrating on writing tests and breaking a problem down into component parts.

Check out the completed version of the project to get an idea what it is. The game is grid based consisting of cells that are either alive or dead. Through each cycle of life this algorithm applies:

  • any live cell with fewer than two live neigbours dies, as if by under-population
  • any live cell with two or three live neighbours lives on to the next generation
  • any live cell with more than three live neighbours dies, as if by overpopulation
  • any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction

For our example we want to see the game in a browser. This is one of the great things about Jest - browser objects such as window and document are already stubbed for us.

We have a basic bootstrap for our project, an html page (includes styles and basic controls), empty test, and the main game file in the game-of-life folder. A convention for Jest is to put test files in a __tests__ folder, so a starting test file exists there.

Running tests

Once we have run npm install we can being testing using jest from the command line. Running this command after each change is quite tedious, so jest has a --watch flag to make tests run when it detects any change to the code.

We can also use Jest to check our code coverage using the coverage flag: jest --coverage. Looking good! Lets now start working on implementing a grid for the game.

DOM

Jest comes with a mock DOM environment, jsdom, which simulates the DOM as if you were in the browser! Again, this is one of the great things about Jest - it covers a lot of bases.

Introduction to a test structure

Before jumping into the tests, we'll cover how to create a test file and structure our tests. Check out the test-example folder.

Definitions

test (or the alias it)
the only part of a test you need, as it runs the assertions within it
describe
creates a code block grouping together several related tests
beforeAll
a function ran before all tests in the current file are run. This helps with resetting variables or performing initiation tasks
afterAll
this is ran after all the tests in the current file have been run. Can be used to clean up environment etc
skip
this makes jest ignore the test e.g. test.skip(...
only
this ignores all tests in the file apart from the one it is called on e.g. test.only(...
beforeEach
this runs a function before every test in the current file. This is helpful for maintaining a default state
afterEach
this runs a function after each test in the current file
expect
this is the assertion function

To start, your tests would be in a folder where your code will be. This makes your code more modular. Inside your code folder you would have a nested folder named __tests__ where you put your individual test files. You can suffix the same filename you are testing with .test or .spec, so for example game.js would become game.spec.js. This helps to see what tests files relates to if you have multiple files.

There is something called 'AAA' - Arrange, Act, Assert. It seems somewhat common sense and we find ourselves falling into this pattern naturally:

  • Arrange: setup for your test; the variables or environment your test depends on
  • Act: calling the function/method you are testing
  • Assert: check the expectations

expect().toBe() and expect().toEqual() both use Object.is for comparasons. However, expect().toBe() is used for primitive values, whereas expect().toEqual() performs a 'deep' comparason so you can compare two different objects for the same values. Check out the jest documentation for further methods.

Example of TDD

We're going to start with the Game of Life algorithm and build it using TDD.

Game algorithm

So, starting small or not at all, we see that we need 2 parameters, whether a cell is alive or dead, and a number of neighbours.

There is one condition that isn't explicitly declared - a dead cell with no neighbours. Lets take this for our first test:

describe("game of life", () => {
  describe("isAlive algorithm", () => {
    it("should return 0 when dead cell (0) with 0 neighbours", () => {
      expect(isAlive(0, 0)).toBe(0);
    });
  });
});

Then run with jest. You should see a failure. Now we can write just enough code to pass the test:

function isAlive() {
  return 0;
}

window.game = {
  isAlive
};

What? I just returned 0! What is the point of that? Well, baby steps. We want to go red to green, red to green in fast short iterations.

Next test, sticking with dead cells, the next test could be for exactly 3 neighbours.

it("should return 1 when dead cell with exactly 3 neighbours", () => {
  expect(isAlive(0, 3)).toBe(1);
});
function isAlive(cell, neighbours) {
  if (neighbours === 3) {
    return 1;
  }
  return 0;
}

Pass! Great! Next tests: live cells.

it("should return 0 when live cell with < 2 neighbours", () => {
  expect(isAlive(1, 0)).toBe(0);
});

Wait, our test passes? We should write a breaking test first? Well, actually looking at the algorithm, it says a live or dead cell with exactly 3 neighbours.

Now to test a live cell with 2 or 3 neighbours:

it("should return 1 when live cell with 2 or 3 neighbours", () => {
  expect(isAlive(1, 2)).toBe(1);
});
function isAlive(cell, neighbours) {
  if (Boolean(cell) && neighbours === 2) {
    return 1;
  }
  if (neighbours === 3) {
    return 1;
  }
  return 0;
}

Now our tests pass. Our code is pretty messy though! This is where we refactor, and we are safe to do so because we have our tests to ensure things are correct.

function isAlive(cell, neighbours) {
  if ((Boolean(cell) && neighbours === 2) || neighbours === 3) {
    return 1;
  }
  return 0;
}

Great! A lot cleaner! In fact we can further optimise this code, even using arrow function to remove the return statement:

const isAlive = (cell, neighbours) =>
  (Boolean(cell) && neighbours === 2) || neighbours === 3 ? 1 : 0;

Remember, as the tests get more specific the code becomes more generic.

We could even update our first test to add further test cases:

describe("algorithm", () => {
  it("should return 0 when dead cell with > 3 or < 3 neighbours", () => {
    expect(isAlive(0, 0)).toBe(0);
    expect(isAlive(0, 1)).toBe(0);
    expect(isAlive(0, 2)).toBe(0);
    expect(isAlive(0, 4)).toBe(0);
  });
});

The solution looks so simple compared to my initial apprehension of complexity of the game. Confidence comes from the tests.

Cells

For us to apply the algorithm we need a cell with neighbours, so lets work on that next. Lets call the function generate, and taking a square root.

describe("generate cells", () => {
  it("should return array of length * length", () => {
    const cells = generate(1);
    expect(cells).toEqual([0]);
  });
});

Course

The above is a small sample of the TDD using Javascript and Jest course on Udemy which goes into more details about the solution.

References

About

Test Driven Development fundamentals wtih Javascript and Jest

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages