Recently, I came across that piece of code I wrote back then to test simple JavaScript functions. I spruced it up, made it pretty, and today I submit that to you for your consideration.
Unit testing basically consists of a unit - usually a very small function - for which you determine both the input and the expected output. The program then does an assert - a comparison between the expected output and the actual output - to determine if the test passes or fails.
You see, vanilla JavaScript has no native assert function. So I made one. This little testing suite is painfully basic - you'd probably do better relying on one of the several JavaScript Unit testing frameworks out there. But it does serve to illustrate how things work.
The setup
Here's some boilerplate HTML.<!DOCTYPE html>
<html>
<head>
<title>Unit testing</title>
<style>
</style>
<script>
</script>
</head>
<body>
</body>
</html>
<html>
<head>
<title>Unit testing</title>
<style>
</style>
<script>
</script>
</head>
<body>
</body>
</html>
For this, we will be testing a function, getCircleArea(). It uses a parameter, r, which is the radius, and then a simple formula to calculate the area.
<script>
function getCircleArea(r)
{
let pi = 3.142;
return pi * r * r;
}
</script>
function getCircleArea(r)
{
let pi = 3.142;
return pi * r * r;
}
</script>
But here's the thing about Unit Testing - you have to write code that is testable. That includes taking care of all the edge cases you can think of. What if r is negative? What if it's not a number?
<script>
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
</script>
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
</script>
That's the function... now let's define the test cases. For this, we use the object testGroups. It's an array of objects. We will have only one group, for now.
<script>
let testGroups =
[
{
}
];
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
</script>
let testGroups =
[
{
}
];
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
</script>
Each group has a name. Call it anything you want, but in the interest of good organization, I would name it after the function we are testing.
let testGroups =
[
{
"name": "getCircleArea"
}
];
[
{
"name": "getCircleArea"
}
];
For every group, there are multiple tests. So make tests an array of objects. Each object has these properties:
title - a description of the test. It's a description, so make it descriptive!
args - an array consisting of all the arguments we are passing into the function to be tested.
unit - the function to be tested.
expected - the value of the output.
let testGroups =
[
{
"name": "getCircleArea",
"tests":
[
{
},
{
},
{
}
]
}
];
[
{
"name": "getCircleArea",
"tests":
[
{
},
{
},
{
}
]
}
];
Below, I have filled in the values for you.
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
Now, create the object unitTesting. It has two methods - prepareDOM() and testUnits().
let testGroups =
[
{
"name": "getCircleArea",
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
}
];
let unitTesting =
{
prepareDOM: function()
{
},
testUnits: function()
{
}
}
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
[
{
"name": "getCircleArea",
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
}
];
let unitTesting =
{
prepareDOM: function()
{
},
testUnits: function()
{
}
}
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
You may as well, at this point, ensure that they fire off when the page is loaded.
<body onload="unitTesting.prepareDOM();unitTesting.testUnits();">
</body>
</body>
prepareDOM() ensures that the DOM has placeholders for your script to populate later. We begin by grabbing the body using the getElementsByTagName() method.
prepareDOM: function()
{
var body = document.getElementsByTagName("body");
},
{
var body = document.getElementsByTagName("body");
},
Then, using a For loop, iterate through the testGroups array.
prepareDOM: function()
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
}
},
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
}
},
Create a fieldset for every element in the array, and append it to the body.
prepareDOM: function()
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
body[0].appendChild(groupFieldset);
}
},
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
body[0].appendChild(groupFieldset);
}
},
Create a legend tag, insert the name of the group, and append it to the fieldset before appending it to the body.
prepareDOM: function()
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
var legend = document.createElement("legend");
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);
body[0].appendChild(groupFieldset);
}
},
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
var legend = document.createElement("legend");
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);
body[0].appendChild(groupFieldset);
}
},
Yep, that's how it's done!
Now iterate through the tests array within each group.
prepareDOM: function()
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
var legend = document.createElement("legend");
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);
for (var j = 0; j < testGroups[i].tests.length; j++)
{
}
body[0].appendChild(groupFieldset);
}
},
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
var legend = document.createElement("legend");
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);
for (var j = 0; j < testGroups[i].tests.length; j++)
{
}
body[0].appendChild(groupFieldset);
}
},
Create a div, give it a unique id, and append it to the fieldset. When you rerun the code, you won't see anything new because we haven't populated these divs.
prepareDOM: function()
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
var legend = document.createElement("legend");
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);
for (var j = 0; j < testGroups[i].tests.length; j++)
{
var testDiv = document.createElement("div");
testDiv.id = "group_" + i + "_test_" + j;
groupFieldset.appendChild(testDiv);
}
body[0].appendChild(groupFieldset);
}
},
{
var body = document.getElementsByTagName("body");
for (var i = 0; i < testGroups.length; i++)
{
var groupFieldset = document.createElement("fieldset");
var legend = document.createElement("legend");
legend.innerHTML = testGroups[i].name;
groupFieldset.appendChild(legend);
for (var j = 0; j < testGroups[i].tests.length; j++)
{
var testDiv = document.createElement("div");
testDiv.id = "group_" + i + "_test_" + j;
groupFieldset.appendChild(testDiv);
}
body[0].appendChild(groupFieldset);
}
},
That's for preparing the DOM; now let's actually test these units using the testUnits() method. Declare two variables - result and exceptionThrown. Because when you run functions, two things basically happen. You might get a result, which is either correct or incorrect. You could have an exception thrown, which is definitely unexpected behavior. We want to cater for all these cases.
testUnits: function()
{
var result, exceptionThrown;
}
{
var result, exceptionThrown;
}
So, again, iterate through the testGroups array and within it, iterate through the tests array.
testUnits: function()
{
var result, exceptionThrown;
for (var i = 0; i < testGroups.length; i++)
{
for (var j = 0; j < testGroups[i].tests.length; j++)
{
}
}
}
{
var result, exceptionThrown;
for (var i = 0; i < testGroups.length; i++)
{
for (var j = 0; j < testGroups[i].tests.length; j++)
{
}
}
}
Declare str, and make it a string that tells us what function is being run, using the title property. Append the string to the div in question.
testUnits: function()
{
var result, exceptionThrown;
for (var i = 0; i < testGroups.length; i++)
{
for (var j = 0; j < testGroups[i].tests.length; j++)
{
var str = "Running " + testGroups[i].tests[j].title + "<br />";
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
}
}
}
{
var result, exceptionThrown;
for (var i = 0; i < testGroups.length; i++)
{
for (var j = 0; j < testGroups[i].tests.length; j++)
{
var str = "Running " + testGroups[i].tests[j].title + "<br />";
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
}
}
}
See what we're doing here? Good.
Now set exceptionThrown to false and result to undefined.
var str = "Running " + testGroups[i].tests[j].title + "<br />";
exceptionThrown = false;
result = undefined;
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
exceptionThrown = false;
result = undefined;
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
Then use a Try-catch. In the Try block, set result to the result returned when we pass in args to the function to be run. For this, we use the apply() method.
var str = "Running " + testGroups[i].tests[j].title + "<br />";
exceptionThrown = false;
result = undefined;
try
{
result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
}
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
exceptionThrown = false;
result = undefined;
try
{
result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
}
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
In the Catch block, we cater for the case where an exception is thrown. If so, set exceptionThrown to true and make the exception message part of str.
var str = "Running " + testGroups[i].tests[j].title + "<br />";
exceptionThrown = false;
result = undefined;
try
{
result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
exceptionThrown = true;
str += err + "<br />";
}
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
exceptionThrown = false;
result = undefined;
try
{
result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
exceptionThrown = true;
str += err + "<br />";
}
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
After that, check if exceptionThrown is true. If not, check result. If result is the same as the expected property, the test is a success.
try
{
result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
exceptionThrown = true;
str += err + "<br />";
}
if (!exceptionThrown)
{
if (result == testGroups[i].tests[j].expected)
{
str += "Test successful.";
passed ++;
}
else
{
}
}
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
{
result = testGroups[i].tests[j].unit.apply(null, testGroups[i].tests[j].args);
}
catch (err)
{
exceptionThrown = true;
str += err + "<br />";
}
if (!exceptionThrown)
{
if (result == testGroups[i].tests[j].expected)
{
str += "Test successful.";
passed ++;
}
else
{
}
}
document.getElementById("group_" + i + "_test_" + j).innerHTML = str;
If not, it's a failure and we want to tell the user why it's a failure by stating what the result was and what we expected instead.
if (!exceptionThrown)
{
if (result == testGroups[i].tests[j].expected)
{
str += "Test successful.";
}
else
{
str += "Test unsuccessful. Expected " + testGroups[i].tests[j].expected + ", returned " + result;
}
}
{
if (result == testGroups[i].tests[j].expected)
{
str += "Test successful.";
}
else
{
str += "Test unsuccessful. Expected " + testGroups[i].tests[j].expected + ", returned " + result;
}
}
Check this out!
Now let's deliberately make the function wrong. What happens?
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r * r;
}
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r * r;
}
Yes, we get a fail case.
Undo that change, and now let's make this guy throw an exception.
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * i;
}
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * i;
}
Great!
Undo that change as well, and now let's add a second function to be tested. It's the getCircleCircumference() function.
function getCircleArea(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
function getCircleCircumference(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * 2;
}
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * r;
}
function getCircleCircumference(r)
{
if (isNaN(r)) return null;
if (r < 0) return null;
let pi = 3.142;
return pi * r * 2;
}
Make a new test group.
let testGroups =
[
{
"name": "getCircleArea",
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
},
{
"name": "getCircleCircumference"
}
];
[
{
"name": "getCircleArea",
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
},
{
"name": "getCircleCircumference"
}
];
And a new series of tests.
let testGroups =
[
{
"name": "getCircleArea",
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
},
{
"name": "getCircleCircumference",
"tests":
[
{
"title": "Circle Circumference should return null if radius is negative.",
"args": [-5],
"unit": getCircleCircumference,
"expected": null
},
{
"title": "Circle Circumference should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleCircumference,
"expected": null
},
{
"title": "Circle Circumference should return correct calculation.",
"args": [5],
"unit": getCircleCircumference,
"expected": (3.142 * 5 * 2)
}
]
}
];
[
{
"name": "getCircleArea",
"tests":
[
{
"title": "Circle Area should return null if radius is negative.",
"args": [-5],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleArea,
"expected": null
},
{
"title": "Circle Area should return correct calculation.",
"args": [5],
"unit": getCircleArea,
"expected": (3.142 * 5 * 5)
}
]
},
{
"name": "getCircleCircumference",
"tests":
[
{
"title": "Circle Circumference should return null if radius is negative.",
"args": [-5],
"unit": getCircleCircumference,
"expected": null
},
{
"title": "Circle Circumference should return null if radius is NaN.",
"args": ['abc'],
"unit": getCircleCircumference,
"expected": null
},
{
"title": "Circle Circumference should return correct calculation.",
"args": [5],
"unit": getCircleCircumference,
"expected": (3.142 * 5 * 2)
}
]
}
];
It's all coming together, isn't it?
This is not complete yet, but we're off to a decent start. It's really bare bones at the moment.
No comments:
Post a Comment