<-- Mailchimp tag -->

It’s Friday afternoon and you have just finished developing your latest cool web app in JavaScript. Feeling comfortable and pat yourself in the back as you’ve followed all the best coding practices. You’ve structured your app in separate modules. You’ve organized your project into folders and subfolders according to each module’s responsibility and made sure you’ve decoupled these modules according to the best software architectural patterns. Also, tested the app yourself a couple of times. Pretending to be a new user clicking every single button in every imaginable combination to make sure that nothing unpredictable comes up. It took quite some time to go through these tests yourself. But you believe that the end-user needs a level of assurance so these monotonous and repetitive manual tests were the way to go.

By Monday morning, you decide it’s time to scale up and get another developer onboard. Your app looks promising! There are now two developers working on the same project simultaneously. Adding new features, optimizing and refactoring the code, restructuring modules and UI components wherever it’s necessary. 

Then, let’s see what is the team progresses at this time ?

As the team progresses, the codebase becomes larger and more complex. The interdependence of various modules proves to be quite fragile and needs constant attention. Checking that each module behaves as expected after another one has been updated, needs constant attention and proves to be time-consuming. Your team feels that you are breaking the DRY/YAGNI/KISS  rules on several occasions. But, are too afraid to do the necessary code refactoring in fear of breaking the app’s functionality.

How does a developer deal with these challenges?

The answer is quite simple: by integrating a testing framework into your code. Manual tests and code reviews are invaluable, but when it comes to constant tests that need to be run every time substantial parts of the code change, automated tests through a testing framework are the way to go. Testing saves time and money.     

Meet QUnit: An easy-to-use JavaScript Testing Framework for the browser with zero-configuration setup.  

Let’s set up a basic test environment, write. some functions and create some tests for them.

Create an HTML file, and add a basic HTML structure:

Now, let’s set up a basic application that will add or subtract two numbers. First, we’ll add the markup:

<body>
  <p>
    <labelfor="num_a">Number A:</label>
    <inputtype="number"id="num_a">
  </p>
  <p>
    <labelfor="num_b">Number B:</label>
    <inputtype="number"id="num_b">
  </p>
  <p>
    <buttonid="add">Add</button>
    <buttonid="sub">Subtract</button>
    <divid="result"></div>
  </p>
</body>

After our markup has been set up, we are ready for a basic JavaScript setup:

<script src="functions.js"></script>
<script>
// Get a reference to the elements that will be used in our app:
const numA   = document.getElementById("num_a");
const numB   = document.getElementById("num_b");
const addBtn = document.getElementById("add");
const subBtn = document.getElementById("sub");
const result = document.getElementById("result");

...
The functions.js file will contain the two main functions that add and subtract numbers:
// script.js
// Define our basic functions:
function add( a, b ){}
function sub( a, b ){}

We also need to add 2 event listeners to the buttons and we are ready to work on the functionality of our app.

<script src="functions.js"></script>
<script>
// Get a reference to the elements that will be used in our app:
const numA   = document.getElementById("num_a");
const numB   = document.getElementById("num_b");
...
// Add Event Listeners to the buttons:
addBtn.addEventListener("click", e =>{
  result.innerText = add( numA.value, numB.value );
});
subBtn.addEventListener("click", e =>{
  result.innerText = sub( numA.value, numB.value );
});
</script>

At this point, we would normally move on to the part where we add the functionality inside the add and sub functions. Instead of doing this, we will follow a much better software development paradigm called TDD (short for Test Driven Development). And we’ll start by writing some tests. In order to do that, we’ll create another page called qunit-tests.html and set up QUnit following the Get Started Guide. This means pasting the following link and script tags which load the test library:

<!DOCTYPE html>
<meta charset="utf-8">
<title>Test Suite</title>
<link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.15.0.css">
<body>
  <div id="qunit"></div>
  <div id="qunit-fixture"></div>
  <script src="https://code.jquery.com/qunit/qunit-2.15.0.js"></script>
</body>

We can now load the functions.js library in the qunit-tests.html (since it contains the functions we want to test) and start adding our first tests:

<script src="https://code.jquery.com/qunit/qunit-2.15.0.js"></script>
<script src="functions.js"></script>
<script>
// Tests will go here
</script>
</body>

<!-- qunit-tests.html -->

Open the qunit-tests.html page in the browser, preferably using a local server (if you are using VSCode, you can launch the web page using the Live Server extension). You’ll be greeted with the following screen:

Finally, we can start adding our first test for the add function.

<script src="https://code.jquery.com/qunit/qunit-2.15.0.js"></script>
<script src="functions.js"></script>
<script>
// Tests will go here
  QUnit.module('add', function() {
    QUnit.test('should add two numbers', function(assert) {
      assert.equal(add(1, 1), 2);
    });
  });
</script>
</body>

<!-- qunit-tests.html -->

If you save and reload (or let Live Server reload automatically for you after you save the file). You’ll see that we now have a failing test. The assertion expects the result of add(1,1) to be 2, but instead, we get undefined.

It’s time to refactor our functions in order for the test to pass. We are going to edit the functions.js file and update the add function as follows:

function add( a, b ){
    return a + b;
}
Wow! Our test is now passing!

But wait a minute! The value type of all HTML input elements is a String! We need to add another test for this case.

// Tests will go here
  QUnit.module('add', function() {
    QUnit.test('should add two numbers', function(assert) {
      assert.equal(add(1, 1), 2);
    });
    QUnit.test('should handle String input', function(assert) {
      assert.equal(add("1", 1), 2);
    });
  });

<!-- qunit-tests.html -->

If you now run the tests, you’ll see that our last test fails. The result of adding “1” and 1 is “11” since JS is always going to concatenate when one of the two operands is a String. We need to refactor our add function to deal with these cases.

function add( a, b ){
    if ( typeof a !== "number" ){
        a = parseFloat( a );
    }
    if ( typeof b !== "number" ){
        b = parseFloat( b );
    }
    return a + b;
}

Run the tests again, and you’ll see that both of them pass. We want to stick with this green beauty for the duration of our development cycle.

Let’s add one more unit test in order to make sure that our add function handles cases of missing input. In these cases, we want to use a default value of 0.

  QUnit.module('add', function() {
    ...
    QUnit.test('should handle missing input', function(assert) {
      assert.equal(add("6",""), 6);
      assert.equal(add("","7"), 7);
    });
  });

<!-- qunit-tests.html -->

Another case of a failing test which leads to code refactoring in order for it to become green and build a safer input handling.

function add( a, b ){
    if ( a === "" ){
        a = 0;
    }
    if ( b === "" ){
        b = 0;
    }
    if ( typeof a !== "number" ){
        a = parseFloat( a );
    }
    if ( typeof b !== "number" ){
        b = parseFloat( b );
    }
    return a + b;
}

This is a very simple and contrived example of a TDD approach with unit tests using QUnit as the testing framework. We start by writing tests, the tests fail so we need to refactor our code and make it pass these tests.

Failing tests > Red > Refactoring > Green > More Failing Tests > Red > More Refactoring > Green.

And then the cycle keeps repeating with more failing tests, more refactoring, until we reach a point where all of our tests turn green and our code is solid and thoroughly tested.

We can even drop the TDD approach (which is a slower development process but yields better and safer results in the longterm) mentioned above and use a simpler and faster process:  start by writing the code and then write the appropriate tests to ensure that the code will always behave as predicted by these tests. Both ways will ensure that we have an automated mechanism that will test our code after each change. It also makes us stick to the habit of splitting our application logic into many small functions (units) that have a single responsibility, can be re-used in other parts of the code or even other applications and be easily tested.  

Our team can now safely work on their code, run the tests after each major refactoring and get notified of any breaking changes. Of course, they have to make sure that each new feature and function must be accompanied by several tests. 

(Now you encouraged to continue the example above and add more tests for both the add and sub functions)

We hope this article helped you get introduced to the QUnit browser testing framework, Unit Testing and Test Driven Development and that you will now code with your testing seat belt fastened

SOURCE CODE

REFERENCES