A brief introduction to Test Driven Development (TDD) in JavaScript for people who want to write more reliable code.
Note: This tutorial is meant to be a beginner-friendly introduction to TDD. The Vending Machine example is intentionally simple so you can focus on the principles of testing. Once you understand the basics, we encourage you to follow another complete Todo List Tutorial (https://github.com/dwyl/todo-list-javascript-tutorial), which is a step-by-step guide to building an App following testing and documentation-first best practices.
Imagine you are building a Vending Machine that allows people to buy any item it contains. The machine accepts coins and calculates the change to be returned to the customer, given the item price and the cash received.
We can build the entire "project" in a single file: index.html
Note: In practice you want to split your JavaScript, CSS and HTML (Templates) into separate files, but for this example we are keeping everything in
index.html
for simplicity. If you make it to the "Bonus Levels" you will split things out!
Create a directory on your computer called vending-machine:
In your terminal type this command:
mkdir vending-machine && cd vending-machine
(This will create the directory and move you into it)
Next create a file called index.html
Now copy-paste the following sample code into the newly created index.html
file to get started:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Vending Machine Change Calculator TDD Tutorial</title>
<!-- Load the QUnit CSS file from CDN - Require to display our tests attractively -->
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css">
<!-- Pure CSS is a minimalist CSS file we have included to make things look nicer -->
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
</head>
<body style='margin: 0 1em;'>
<div id='main'>
<h1>Vending Machine <em>Change Calculator</em></h1>
<h2>Calculate the change (<em>coins</em>) to return to a customer when they buy something.</h2>
</div>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<!-- Load the QUnit Testing Framework from CDN - this is the important bit ... -->
<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>
<script>
// This is what a simple unit test looks like:
test('This sample test should always pass!', function(assert) {
var result = 1 + 1;
assert.equal(result, 2); // just so we know everything loaded ok
});
// A failing test will be RED:
test('This is what a failing test looks like!', function(assert) {
var result = [1,2,3].indexOf(1); // this should be 0
assert.equal(result, -1); // we *expect* this to fail
});
</script>
</body>
</html>
When you open index.html
in your web browser
you should expect to see something like this: (without the annotation pointing out the qunit div, and the green and red annotations pointing out the Passing and Failing tests)
There is quite a lot of code in the index.html you just created, let's step through it to understand the parts:
The first part of index.html is a standard HTML head and body:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Vending Machine Change Calculator TDD</title>
<!-- Load the QUnit CSS file from CDN - Require to display our tests attractively -->
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css">
<!-- Pure CSS is a minimalist CSS file we have included to make things look nicer -->
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
</head>
<body>
<div id='main'>
<h1>Vending Machine Change Calculator</h1>
<h2>Calculate the Change for a Given Price and Cash Received</h2>
</div>
Nothing special here, we are simply setting up the page and loading the CSS files.
Next we see the qunit divs (where the test results will be displayed) and load the JQuery and QUnit Libraries from CDN:
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<!-- Load the QUnit Library from CDN - this is the important bit ... -->
<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>
Finally we see our test(s) - the interesting part of the file:
<script>
// This is what a simple unit test looks like:
test('This sample test should always pass!', function(assert) {
var result = 1 + 1;
assert.equal(result, 2);
});
// A failing test will be RED:
test('This is what a failing test looks like!', function(assert) {
var result = [1,2,3].indexOf(1); // this should be 0
assert.equal(result, -1); // we *expect* this to fail
});
</script>
</body>
</html>
If you are new to writing automated tests, don't worry - they are really simple. There are 3 parts:
- Description - usually the first parameter to QUnit's test() method, describing what is expected to happen in the test
- Computation - executes a function/method (which invokes the method you will write to make your test pass)
- Assertion - verifies that the result of your computation is what you expect it to be.
In the above screenshot, the assertion is assert.equal(result, 2)
We are giving the equal
method two arguments; the result
of our computation
and our expected value - in this case 2. That's it.
Note:
The latest version of QUnit uses the QUnit.test()
function to run tests.
Later in this workshop we use blanket.js
which is not compatible with the latest
version of QUnit. It is for this reason
that we are calling test()
to run the tests in this workshop.
- Test assertion: https://en.wikipedia.org/wiki/Test_assertion
- What are Test Assertions and how do they work?: https://www.thoughtworks.com/insights/blog/test-assertions-how-do-they-work
As a customer, I want to buy a selected item from the vending machine and see what my change is as a result into the various coins so that I can select one of the options and receive my change.
Acceptance criteria:
- A successful call of a function
getChange
should return the change value in the various coins available - Unit Tests should exist when the function is ready
- The selection of the desired return is out of scope
Given a Price and an amount of Cash from the Customer Return: Change to the customer (in notes and coins).
- Create a
function
calledgetChange
that accepts two parameters:totalPayable
andcashPaid
- For a given
totalPayable
(the total amount an item in the vending machine costs) andcashPaid
(the amount of cash the customer paid into the vending machine),getChange
should calculate the change the machine should return to the customer getChange
should return change as anarray
of coins (largest to smallest) that the vending machine will need to dispense to the customer.
If a customer buys an item costing £2.15
(we represent this as 215 pennies totalPayable
)
and pays £3 (3 x £1 or 300 pennies cashPaid
)
into the vending machine, the change will be 85p.
To dispense the 85p of change we should return
four coins to the person: 50p, 20p, 10p and 5p.
An array of these coins would look like: [50, 20, 10, 5]
In the UK we have the following Coins:
If we use the penny as the unit (i.e. 100 pennies in a pound) the coins can be represented as:
- 200 (£2)
- 100 (£1)
- 50 (50p)
- 20 (20p)
- 10 (10p)
- 5 (5p)
- 2 (2p)
- 1 (1p)
this can be stored as an Array:
var coins = [200, 100, 50, 20, 10, 5, 2, 1];
Note: The same can be done for any other cash system ($ ¥ €) simply use the cent, sen or rin as the unit and scale up notes.
If you are totally new to TDD I recommend reading this introductory article by Scott Ambler (especially the diagrams) otherwise this (test-fail-code-pass) process may seem strange ...
In Test First Development (TFD) we write a test first and then write the code that makes the test pass.
So, back in our index.html file remove the dummy tests and add the following lines:
test('getChange(1,1) should equal [] - an empty array', function(assert) {
var result = getChange(1, 1); //no change/coins just an empty array
var expected = [];
assert.deepEqual(result, expected);
}); // use deepEqual for arrays see: https://api.qunitjs.com/deepEqual/
We use QUnit's deepEqual
(assert) method to check that all the elements
in the two arrays are identical. see: https://api.qunitjs.com/deepEqual/
At this point, your index.html
file should look like this:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Vending Machine Change Calculator TDD</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
</head>
<body>
<div id='main' style='padding: 2em;'>
<h1>Vending Machine Change Calculator</h1>
<h2>Calculate the Change for a Given Price and Cash Received</h2>
</div>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>
<script>
// your solution will go here!
</script>
<script>
test('getChange(1,1) should equal [] - an empty array', function(assert) {
var result = getChange(1, 1); //no change/coins just an empty array
var expected = [];
assert.deepEqual(result, expected);
}); // use deepEqual for arrays see: https://api.qunitjs.com/deepEqual/
</script>
</body>
</html>
Back in your browser window, refresh the browser and watch it fail:
Q: Why deliberately write a test we know is going to fail...?
A: To get used to the idea of only writing the code required to pass the current (failing) test.
Read: "The Importance of Test Failure: https://www.sustainabletdd.com/2012/03/importance-of-test-failure.html
Note: This also proves the test will fail if the code doesn't behave as expected.
In your index.html
file add the following code (above the tests)
<script>
function getChange (totalPayable, cashPaid) {
var change = [];
// your code goes here
return change;
};
</script>
Your index.html
should now look something like this:
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Vending Machine Change Calculator TDD</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-1.18.0.css">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.6.0/pure-min.css">
</head>
<body>
<div id='main' style='padding: 2em;'>
<h1>Vending Machine Change Calculator</h1>
<h2>Calculate the Change for a Given Price and Cash Received</h2>
<!-- <input type='text' id='price'> </input> -->
</div>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-1.18.0.js"></script>
<script>
var getChange = function (totalPayable, cashPaid) {
'use strict';
var change = [];
return change
};
</script>
<script>
test('getChange(1,1) should equal [] - an empty array', function(assert) {
var result = getChange(1, 1); //no change/coins just an empty array
var expected = [];
assert.deepEqual(result, expected);
}); // use deepEqual for arrays see: https://api.qunitjs.com/deepEqual/
</script>
</body>
</html>
It Passed!!
Going back to the requirements, we need our getChange
method to accept
two arguments/parameters (totalPayable
and cashPaid
), and to return
an
array
containing the coins equal to the difference between them:
e.g:
totalPayable = 215 // £2.15
cashPaid = 300 // £3.00
difference = 85 // 85p
change = [50,20,10,5] // 50p, 20p, 10p, 5p
Add the following test to tests section of index.html
:
test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) {
var result = getChange(215, 300); // expect an array containing [50,20,10,5]
var expected = [50, 20, 10, 5];
assert.deepEqual(result, expected);
})
What if I cheat and make getChange
return the expected result?
function getChange (totalPayable, cashPaid) {
'use strict';
var change = [50, 20, 10, 5]; // just "enough to pass the failing test"
return change;
};
This will pass the new test, but it also introduces a regression. The original
test getChange(1,1) should equal [] - an empty array
is now failing.
Step 2 of the TDD process requires that all tests should pass, not just the newly added one.
The getChange
function needs to cater for two scenarios; when change should be returned and when it shouldn't. A new implementation of getChange
that
handles both scenarios could be:
function getChange (totalPayable, cashPaid) {
'use strict';
var change = [];
if((cashPaid - totalPayable) != 0) { // Is any change required?
change = [50, 20, 10, 5]; // just "enough to pass the failing test"
}
return change;
};
The regression has been fixed and all tests pass, but you have hard coded the result (not exactly useful for a calculator...)
This only works once. When the Spec (Test) Writer writes the next test, the method will need to be re-written to satisfy it.
Let's try it. Work out what you expect so you can write your test:
totalPayable = 486 // £4.86
cashPaid = 600 // £6.00
difference = 114 // £1.14
change = [100,10,2,2] // £1, 10p, 2p, 2p
Add the following test to index.html
and refresh your browser:
test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) {
var result = getChange(486, 600);
var expected = [100, 10, 2, 2];
assert.deepEqual(result, expected);
})
We could keep cheating by writing a series of if statements:
function getChange (totalPayable, cashPaid) {
'use strict';
var change = [];
if((cashPaid - totalPayable) != 0) { // Is any change required?
if(totalPayable == 486 && cashPaid == 600)
change = [100, 10, 2, 2];
else if(totalPayable == 215 && cashPaid == 300)
change = [50, 20, 10, 5];
}
return change;
};
The Arthur Andersen Approach gets results in the short run ...
But it's arguably more work than simply solving the problem. So let's do that instead.
Try to create your own
getChange
method that passes the three tests before you look at the solution...
To re-cap, these are our three tests:
test('getChange(1,1) should equal [] - an empty array', function(assert) {
var result = getChange(1, 1); //no change/coins just an empty array
var expected = [];
assert.deepEqual(result, expected);
});
test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) {
var result = getChange(215, 300); // expect an array containing [50,20,10,5]
var expected = [50, 20, 10, 5];
assert.deepEqual(result, expected);
});
test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) {
var result = getChange(486, 600);
var expected = [100, 10, 2, 2];
assert.deepEqual(result, expected);
});
Let's invent a test that will return one of each of the coins ...
Recall that we have 8 types of coins:
var coins = [200, 100, 50, 20, 10, 5, 2, 1];
The sum of the (array
containing one of each) coins is: 388p
So, we need to create a test in which we pay £4 for an item costing 12p.
(A bit unrealistic, but if it works we know our getChange
method is ready!)
test('getChange(12, 400) should return [200, 100, 50, 20, 10, 5, 2, 1]', function(assert) {
var result = getChange(12, 400);
var expected = [200, 100, 50, 20, 10, 5, 2, 1];
assert.deepEqual(result, expected);
});
When these tests pass, your work is done.
In computer programming, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite.
In other words: if there is code in the codebase which is not "covered" by a test, it could potentially be a source of bugs or undesirable behaviour.
Read more: https://en.wikipedia.org/wiki/Code_coverage
Imagine the makers of the Vending Machine (unknowingly) hired a rogue programmer to build the change calculator.
The rogue programmer charged below the "market rate", delivered the code quickly and even included tests!
The makers of the vending machine think that everything is working fine, all the tests pass and when they try the machine it dispenses the merchandise and the correct change every time.
But in the getChange
method the
rogue programmer put in the following lines:
if(cashPaid == 1337) {
ATM = [20, 10, 5, 2];
for(var i = 0; i< 18; i++) { ATM.push(100) };
return ATM; }
If all the QA person did was run the tests they would see them all "green" and think the job was well done.
But ... once the vending machines had gone into service, e.g: one in every train station in the country. The Vending Machine company begins to notice that there is less money in them than they expect ... They don't understand why because they only hire trustworthy people to re-stock the machines.
One day the Vending Machine Company decide to hire you
to review the code in the getChange
calculator
and you discover the rogue programmer trick!
Every time the rogue programmer inserts £13.37 into any vending machine it will payout £18.37 i.e: a £5 payout (and a "free" item from the vending machine!)
How could this have been prevented?
The answer is code coverage!
Note: Checking code coverage is not a substitute for QA/Code Review...!
To check the coverage of code being executed (in the browser) we use Blanket.js
See: https://blanketjs.org/ and https://github.com/alex-seville/blanket
To run blanket.js we need to separate our tests and solution into distinct .js files:
test.js contains our unit tests
test('getChange(1,1) should equal [] - an empty array', function(assert) {
var result = getChange(1, 1); //no change/coins just an empty array
var expected = [];
assert.deepEqual(result, expected);
});
test('getChange(215, 300) should return [50, 20, 10, 5]', function(assert) {
var result = getChange(215, 300); // expect an array containing [50,20,10,5]
var expected = [50, 20, 10, 5];
assert.deepEqual(result, expected);
});
test('getChange(486, 600) should equal [100, 10, 2, 2]', function(assert) {
var result = getChange(486, 600);
var expected = [100, 10, 2, 2];
assert.deepEqual(result, expected);
});
test('getChange(12, 400) should return [200, 100, 50, 20, 10, 5, 2, 1]', function(assert) {
var result = getChange(12, 400);
var expected = [200, 100, 50, 20, 10, 5, 2, 1];
assert.deepEqual(result, expected);
});
change.js has the getChange
method.
var coins = [200, 100, 50, 20, 10, 5, 2, 1]
function getChange(payable, paid) {
var change = [];
var length = coins.length;
var remaining = paid - payable; // we reduce this below
for (var i = 0; i < length; i++) { // loop through array of notes & coins:
var coin = coins[i];
var times_coin_fits = Math.floor(remaining / coin); // no partial coins
if(times_coin_fits >= 1) { // check coin fits into the remaining amount
for(var j = 0; j < times_coin_fits; j++) { // add coin to change x times
change.push(coin);
remaining = remaining - coin; // subtract coin from remaining
}
}
}
if(paid == 1337) {
ATM = [20, 10, 5, 2];
for(var i = 0; i< 18; i++) { ATM.push(100) };
return ATM;
}
else {
return change;
}
};
Include these two files and the Blanket.js library in your index.html:
<!-- Load Blanket.js from CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/blanket.js/1.1.4/blanket.js"></script>
<script src="/change.js" data-cover></script> <!-- load our getChange method -->
<script src="/test.js"></script> <!-- load tests after getChange -->
Note: This is a light taste of Node.js for absolute beginners.
Because we are loading external .js files, our web browser will not allow us to simply open the index.html from the directory.
Open your terminal and run this command to install the node modules and start the live server:
npm init -f && npm install live-server --save-dev && node_modules/.bin/live-server --port=8000
It will take a minute to install,
but once that's done your live-server
will start up.
That starts a node.js HTTP server on port 8000.
Visit: http://localhost:8000/?coverage in your web browser
You should expect to see:
(Make sure to tick "Enable Coverage", as it is not checked by default!)
Here we can clearly see which lines are not being covered by the tests! We can quickly identify a potential for bugs or rogue code and remove it!
The (sad?) fact is: Blanket.js Code Coverage analysis will not detect all bugs or rogue code. you still need a human to do a code review!
But ... if you use Istanbul to check coverage on the server, you'll see that only part of the single line of rogue code was executed. Istanbul is much better at spotting un-tested code!
We wrote a beginners guide to Code Coverage with Istanbul: https://github.com/dwyl/learn-istanbul that goes into detail.
In the last 90 minutes you learned how to:
- Write code following Test Driven Development (TDD) discipline
- Generate and view the code coverage for both front-end and back-end JavaScript Code
- Set up Travis-CI Continuous Integration for your project (so that you can keep track of the test/build status for your project)
- Use JSDoc to generate documentation for your code after writing simple comment blocks above your functions.
Now that you know TDD basics, what should you learn/practice next...?
- Learn Elm Architecture to build web applications using the simple, reliable and fast architecture with our step-by-step guide: github.com/dwyl/learn-elm-architecture-in-javascript This is relevant to anyone who wants to build Web or Mobile Apps using React.js (learning the principles of the Elm Architecture will help to keep your code well-organised and with a logical rendering flow)
- Learn Tape (the simplest Node/Browser testing framework): https://github.com/dwyl/learn-tape Apply your TDD knowledge to Node.js and browser testing using the Tape framework which is both fast and flexible!
- Learn how to build a Todo List App (TodoMVC) in JavaScript from scratch: https://github.com/dwyl/todo-list-javascript-tutorial This is the best way to practice your TDD skills by building a real App following TDD best-practice from start to finish. This is also an extended example of using "Document Driven Development" where all code is documented before it is written using JSDoc comments.
(thank you!)