Testing your JavaScript Code — TDD

In this article, I will be making use of the Jest testing framework. There are many other frameworks for testing but my current favourite is Jest and I will be using it in this article.

First things first, **what is testing? **Testing (i.e software testing) is an investigation conducted to provide stakeholders with information about the quality of the software product or service under test.

That might have sounded like a lot, let me break it down. Software testing involves the process of checking your code either manually or automatically (well semi-automatically) to make sure the code functions as needed.

The test we will be referring to in this article is the automated test, the tests you want to create to help you find all (or as many as possible) edge cases in your code. I will talk more on the automated tests soon, but first let’s talk about why we test codes.

Types of Test

  • **Unit testing — **is the level of testing that checks individual units (components) of our code in isolation from others, purpose is to ensure each unit of the code functions as designed.
  • **Integration testing — **is the level of testing that combines units as a group and tests how they work together, purpose is to expose the faults that might exist in the interaction individual units.
  • **End-to-End testing — **is the level of testing that checks the flow of a system from beginning to end as a single application and that it behaves as expected, purpose is to identify system errors and strengths and to ensure that the data on the system is secure across all levels of the system.

Why we should test our code

Main reason for testing your code is to have a very stable code, you want your code to do exactly what you want it to do, and you do not want to have to do this test manually, by checking each part. What you do is called an automated test, you use testing frameworks like **Jest. **You write a test suite expecting your code (function) to work in a certain way or return a particular value, if your code does pass this, you know you need more edge cases in your test, except you have enough and have covered it all, but when your test fails, you go back to your code / functionality, you refactor the code and test again — this process is called regression testing.

The popular saying in testing is that your test is meant to fail the first time you run the test.

TDD — Test Driven Development

This is the process of developing a product (software), that relies on repetitions of test and develop cycle, the automated test is meant to come first and fail first time it is run, then you write the function for the test.

The following sequence of steps is generally followed:

  • Add a test
  • Run all tests and see if the new one fails (if there are old tests)
  • Write some code
  • Run tests
  • Refactor code
  • Repeat

The way the Testing framework (in our case — jest) knows the file that it should read the tests from is by checking for any file with the .test.jsor .spec.js extension. I must warn you, TDD requires much more consideration and time than ‘regular’ programming.

Let’s setup

To be able to perform all tests below or even write your own tests, you need to setup your environment for that.

// In your terminal do the following
$ npm init

$ npm install --save-dev jest
// Then in your package.json file
// change the value of test in scripts to jest

{
  "scripts": {
    "test": "jest"
  }
}

Enough talk, let’s get our hands dirty — not literally. We will write test for a mini project I created earlier to explain OOP, I already did the testing while building the mini project, I will simply be walking you through what testing is about and how you can perform various tests, then I will show you what I did while testing an earlier created project.

The first thing I did was check that when I create an instance of a Constructor function, it does work. That way I am sure that my constructor is created properly and then I can continue my work from that point.

Then I wrote my constructor function, and keep refactoring it, until it passed my above test.

Let us take a small detour, I will do us a small test and sum function here, so you can see the entire process. First thing we will do is to write a test we think should work for a sum function, which should be something like 2+3 = 5.

So first thing as best practise is to write the test first;

const sum = require('./sum') // assuming name of file will be sum 
                            // and they are in the same directory
test(‘Add 1 and 2 to give 3’, () => {
    expect(sum(1,2)).toBe(3);
});

This will fail the first time because we are yet to write our sum function, then we will write the sum function to make sure we pass this test

function sum(a, b) { return a + b }
module.exports = sum; // so we can import it in the Jest file

This is straightforward and as we can see this will work if we test in our console or even terminal (maybe using node). Now we need to consider edge cases, for instance there is this one you may or may not know,

0.1 + 0.2 = 0.3 // wrong
0.1 + 0.2 = 0.30000000000000004 // correct and this is not only in JavaScript

So how to we test for this edge case, we can go ahead using other matchers to resolve that kind of issue, we can also check to be sure it returns a value (crooked way to go). For this, we will check to make sure it returns a value like 0.300…4.

test(‘Add 0.1 and 0.2 to be close to 0.3’, () => {
    expect(sum(0.1, 0.2)).toBeCloseTo(0.3);
}

Reason I chose to use **toBeCloseTo **as the matcher (method) for this is because when dealing with decimal values, there are possibilities for more values after the decimal point, depending on what computation is performed on the values, so instead of looking for the exact, I am making sure it is somewhere in that range.

Now you can see how we went from a test, wrote the function to pass the test, expanded our edge case, wrote a test for it and then we will still pass with our function. You can now go ahead to make sure the parameters are always numbers, also test for negative values.

Find below a test suite, the sum function we have above will fail some of the test, you will need to refactor it to pass all the tests.

// this is to connect the sum function with the test file, so Jest knows where the functions are.
const sum = require('./sum');

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3);
});

test('add -1 to 3 to equal 2', () => {
  expect(sum(-1, 3)).toBe(2);
});

test('add a string and a Number to equal NaN', () => {
  expect(sum('tolu', 3)).toBe('NaN');
});

test('adds two strings to equal NaN', () => {
  expect(sum('decagon', 'institute')).toBe('NaN');
});

test('add two negative numbers -4 and -9 to equal -13', () => {
  expect(sum(-4, -9)).toBe(-13);
});

test('enter undefined values to output - enter parameters', () => {
  expect(sum()).toBe('You have not entered any paramter')
});

test('adds numbers in string \'23\' to \'3\' to equal 26', () => {
  expect(sum('23', '3')).toBe(26);
});

test('Insert NaN as the only argument to equal NaN', () => {
  expect(sum(NaN)).toBe('NaN');
});

test('Adding special characters like ! and ( to equal NaN', () => {
  expect(sum('!', '*')).toBe('NaN');
});

test('Adding 2 Decimal numbers - 0.1 and 0.2 to be close to 0.3', () => {
  expect(sum(0.1, 0.2)).toBeCloseTo(0.3);
});

test('Adding 2 values 0.915 and 0.11 to equal 1.025', () => {
  expect(sum(0.915, 0.11)).toBeCloseTo(1.025);
});

Before we go deep, let me introduce you to Jest expect Api

The Jest expect Api has a number of matchers you can use, It looks like Jest already accounted for all the things possible to be tested, you can test for truthiness, numbers, objects, arrays containing certain values, and much more. When you’re writing tests, you often need to check that values meet certain conditions. expect gives you access to a number of “matchers” that lets’ you validate different things. We obviously can’t talk about all the matchers or methods possible, we will however talk about .toBe() , .toMatch() , and .toEqual() . To learn more about expectvisit the Jest expect Api documentation page.

.toBe()

.toBe() is used to compare primitive values, It calls Object.is to compare values, which is better for testing than === , according to the Jest documentation. You should not use .toBe() with floating point values, like in the case of 0.1 + 0.2 above, it does not resolve to 0.3 , making .toBe()inappropriate for the test, but .toBeCloseTo() is the right method to use for floating-point numbers.

test(‘Add 1 and 2 to give 3’, () => {
    expect(sum(1,2)).toBe(3); 
});

.toMatch()

.toMatch() is used to compare a string against a regular expression (regex) or against another string. For example, you might not know what a function returns but you know it should have a particular text in it, you can create a regex to test to be sure it is the correct thing that it returns; let us use returnString() as our function, it returns 'I am learning testing with you, yes you!' , remember we do not know it returns the above, we are going to then write a test to check using regex and also another test for when we are sure of what we want it to return, we can also use just a part of the expected string.

test(‘A statement on learning’, () => {
    expect(returnString()).toMatch(/testing/); // using regex
    expect(returnString()).toMatch('I am learning testing with you, yes you') // using the exact string you want returned
    expect(returnString()).toMatch('I am learning ') // using just a part of the string
});

.toEqual()

.toEqual() is used to compare that two objects are same to the detail (checking all properties of the Object). It also calls Object.is to compare values, which is better for testing than === (strict equality), according to the Jest documentation.

const obj1 = {
    isArticle: true,
    title: 'Testing your JavaScript Code — TDD'
}
const obj2 = {
    isArticle: true,
    title: 'Testing your JavaScript'
}
const obj3 = {
    isArticle: true,
    title: 'Testing your JavaScript Code — TDD'
}
test(‘Checking two objects to make sure they are qual’, () => {
    expect(obj1).toEqual(obj2); // wrong
    expect(obj1).toEqual(obj3); // correct
});

You can run the above to be sure. You should also check out the Jest Documentation to learn more about Jest and testing in general.

With our not so little detour, I guess you should have familiarised yourself with Jest, it’s expect Api and what is happening in our test file. Now back to the subject matter, we will add some more test for our OOP functions, I will explain some of the tests here and show you what your output should look like when you are yet to pass your tests and when you do pass all. If you want a peek at the full test suite and the functions, you should clone the repository.

So let’s add the test cases;

// Import the necessary files for the test to work.

// ...

test('Creating a student Record', function() {
  var jon = new User('Jon Snow', 'jonsnow@housestark.got', 'iknownothing', 'teacher');
  var arya = new User('Arya Stark', 'aryastark@housestark.got', 'agirlhasnoface');
  expect(jon.createRecord('4.3', 'Very brilliant chap', '0032')).toBe('Student Record Successfully added');
  expect(arya.createRecord('3.0', 'Decent Attempt', '0012')).toBe('You do not have permission to do this');
});

test('Reading a record by Id', function() {
  var cersei = new User('Cersei Lannister', 'ceisei@houselannister.got', 'neckkk', 'admin');
  var sansa = new User('Sansa Stark', 'sansa@housestark.got', 'ithoughtyouwerethewisest');
  expect(cersei.readById(1)).toEqual(expect.objectContaining({grade : '4.3'}));
  expect(sansa.readById(1)).toMatch('You do not have per');
})

// ...

test('Editing a Student Record', function() {
  var jon = new User('Jon Snow', 'jonsnow@housestark.got', 'iknownothing', 'teacher');
  var petyr = new User('Petyr Baelish', 'petyr@housebaelish.got', 'iamlittlefinger', 'parent');
  expect(jon.updateStudentRecord(1, '4.5', 'This young chap keeps breaking his own records')).toMatch('Student Record');
  expect(petyr.updateStudentRecord(1, '4.34', 'Nothing lasts, so keep working hard')).toBe('You do not have permission to do this');
});

test('Deleting a student Record', function() {
  var jon = new User('Jon Snow', 'jonsnow@housestark.got', 'iknownothing', 'teacher');
  var cersei = new User('Cersei Lannister', 'ceisei@houselannister.got', 'neckkk', 'admin');
  expect(cersei.createRecord('4.25', 'His head is really on his neckkk', '0117')).toBe('Student Record Successfully added');
  console.log(db.studentRecords);
  expect(cersei.deletedAStudentRecord(2)).toBe('Student Record Deleted');
  expect(jon.deletedAStudentRecord(1)).toMatch('You do not have ');
  console.log(db.studentRecords);
});

The first test case above is for the creation of a student record, we first instantiate the user we want to use to create the record from the User Constructor, then we call the .createRecord method on the user — jon.createRecord('4.3', 'Very brilliant chap', ‘0032’) , and then we use the .toBe() matcher, to check if it returns the exact return value. Notice me checking for when a student (user without permission) is trying to do same function and what it returns.

The second test checks for when an Admin reads a student record by its’ Id. As you can see we have use the .toEqual() and .toMatch() methods in this test. Like the other cases, we create a user to test, we then perform the .readByIdmethod — cersei.readById(1) , we then use the .toEqual() matcher and then you can see the expect.objectContaining() — this part now expects a return value that will be an object and should contain what is stated in the parenthesis — trick here is to add the object literal {} in the parenthesis before the key: 'value' pair, this will help resolve it into an object. And then on the next line we are testing for when a user without permission attempts to carry out the function and then we use the .toMatch() method and then we test against a part of the full string.

Coverage

This is one part of jest I love so much, it helps you know how much of your code you have covered. You are able to know the parts of your code you are yet to test, and you know you have done a good job when you hit full 100 on all parts of your code.

As you can see, I have not hit 100 on all, with a few edge cases tested I am able to complete this coverage to hit a full 100. You should strive for 100 but do not kill yourself in the process (i.e. do not wast valuable time), there are more things to do than trying to hit 100 but before you stop trying to kill yourself make sure you hit a 92% at the least.

To get coverage in your tests add --coverage to your scripts in package.json .

{
  "scripts": {
    "test": "jest --coverage"
  }
}

Below is my complete Suite — with 100% coverage.

beautiful isn’t it 😍

Running Tests

Running a test suite or just one test or any test at all is really simple and straightforward, all you need is npm run test . When you enter this in your terminal and tap on your enter button (while you are in your project folder), jest will find the test files and run the test.

#javascript #testing

Testing your JavaScript Code — TDD
28.65 GEEK