The importance of testing
About a year ago, I was brought into a start-up to help them finish development in time for their deployment date. This company's product dealt with large numbers of small transactions, and so accuracy was essential.
I was working on the front end, but I immediately noticed an issue: the calculations weren't adding up. We would put in a few hundred transactions, and the totals would be off by tens of euros. Something was going seriously wrong.
This project was large, complex, and very ambitious (the front end alone had more than 600,000 lines of code!), but this start-up had not implemented any testing framework. Tracking down the problem took us over a week with mounting frustration and panic. There were a quarter million euros in play, and that was just for one client.
This story ended well, but it took a whole week of digging for one error, which would have been caught if there had been decent testing. We could have lost enough money that the company would have gone bust.
What is testing?
Well, that depends! There are three main types.
Unit tests
Unit testing checks individual units (generally single functions or classes) by providing them with input and making sure they generate the expected output.
Generally, each unit is tested for a simple case, and then for one or more edge cases.
For example, if you take a couple of functions from a previous chapter:
const getWordCount = (stringToTest) => {
const wordArray = stringToTest.split(' ');
return wordArray.length;
}
const getLetterCount = (stringToTest) => {
const wordArray = stringToTest.split(' ');
let totalLetters = 0;
for (let word of wordArray) {
word.replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
totalLetters += word.length;
}
return totalLetters;
}
Which cases would you check for each function here?
getWordCount
— You would check a string where you know the number of words (simple case), then perhaps an empty string, and a string containing only spaces (edge cases).getLetterCount
— You would check string where you know the number of letters (simple case); then try a string with only punctuation (edge case).
You could write these tests as helper code:
const testSimpleWordCount = () => {
const testString = 'I have four words!';
if (getWordCount(testString) !== 4) {
console.error('Simple getWordCount failed!');
}
}
const testEdgeWordCount = () => {
const testString = ' ';
if (getWordCount(testString) !== 0) {
console.error('Edge getWordCount failed!');
}
}
const testSimpleLetterCount = () => {
const testString = 'I have twenty one letters!';
if (getLetterCount(testString) !== 21) {
console.error('Simple getLetterCount failed!');
}
}
const testEdgeLetterCount = () => {
const testString = '")(&;//!!';
if (getLetterCount(testString) !== 0) {
console.error('Edge getLetterCount failed!');
}
}
These are simple tests, and can be OK for quick checks, but it is generally better to use a testing framework.
Testing frameworks and libraries allow you to write whole test suites to test your code automatically, using special functions and syntax. Here is what the first two tests above might look like in certain frameworks:
describe('getWordCount()', function() {
it('should find four words', function() {
expect(getWordCount('I have four words!').to.equal(4));
});
it('should find no words', function() {
expect(getWordCount(' ').to.equal(0));
});
});
Unit tests generally make up between 60% and 80% of overall tests in JavaScript projects. Next up, we have:
Integration tests
Integration tests check multiple functions or classes to make sure that they work together as they are supposed to. The above image shows what can happen when the individual units work properly (both drawers open properly in isolation), but their integration in the surrounding system causes a malfunction.
Functional tests
Functional tests, also known as end-to-end (E2E) tests, check whole scenarios in context. For example, a user signs in to your app, opens their notifications, and marks them all as read. These tests also check any external resources your project may use, like a third-party payment system, for example.
Let's recap!
In this chapter, you were introduced to the three types of test:
Unit tests
Integration tests
Functional (E2E) tests
You also saw that, while manual helper code tests can work, there are automated frameworks which make testing easier and more reliable.
In the next chapter, we will have a look at how to fix code when it goes wrong: debugging!