DEV Community

Cover image for Testing Code
Konstantin Stanmeyer
Konstantin Stanmeyer

Posted on • Updated on

Testing Code

Why Test?

Writing test scripts allows us to check if data is flowing correctly even before running an application, determining received products meet out expectations. We can run desired conditions through our code, and prior to deployment, especially, we can ensure that even the most extreme use-cases will not result in an app-breaking bug. (We want to make sure getting any values or resources we don't want will ever happen, prior to deployment)

Overview

The flow should work as: You find something you wouldn't like to happen, so before a user can even let that happen, we must imitate how the values are sent or mutated, and ensure there are no hole in our code.

This setup will be using three separate packages to perform three different, but cohesively work to accomplish these:

  1. Mocha: This executes any test code that we have defined, as well as returning information on whether the test(s) passed or failed.

  2. Chai: The code that defines what passes or fails a test.

  3. Sinon: Replaces some of our predefined functions (explained later)

NOTE: This will focus on a "unit test" (generally one function). What I will not cover is an "integration test", which is more complicated but can allow us to assess a flow of actions that work together. (e.g. check if route fires correctly, if the middleware functions, if controllers work properly, etc.)

Simple Testing:

First, set up package.json and install the dependencies to the developer environment:

npm init

npm i --save-dev mocha chai
Enter fullscreen mode Exit fullscreen mode

We can take a look in our package.json file and see a test script with some useless code:

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
},
Enter fullscreen mode Exit fullscreen mode

We can change that line to just "Mocha":

"scripts": {
    "test": "mocha"
},
Enter fullscreen mode Exit fullscreen mode

The next step is to create an actual place for Mocha to reference, as well as where we will store the tests.

Create a new folder named "test" at the root of your project. Now create a .js file within it having any name.

The very cool part of writing tests is how natural they flow.

Tests start with a function given by Mocha:

// ./test/example.js

it('should equal 5', function(){
  ...
})
Enter fullscreen mode Exit fullscreen mode

Which is given two parameters: We can define a message, as well as an anonymous function which will contain the actual conditions. This is meant to read like a normal sentence.

To define the success conditions, here is where we use Chai. It utilizes three key methods from the package, only slightly different in syntax but can accomplish identical results, leaving the choice up to your syntactical preference. Here are the official docs for more understanding:

Image description

That along with their docs are here.

In the same test file previously used (example.js), we can import the .expect() method from Chai, and an extremely basic, redundant test looks like so:

// ./test/example.js

const expect = require('chai')(expect);

it('should equal 5', function(){
  const num1 = 3
  const num2 = 2

  expect(num1 + num2).to.equal(5);
})
Enter fullscreen mode Exit fullscreen mode

This syntax is crazy just how grammatically correct it is; It doesn't feel like coding. If we were to use the .assert() method instead of .expect(), it could look like this:

assert.equal(num1 + num2, 5); // second arg is the expected
Enter fullscreen mode Exit fullscreen mode

To run theses tests we can type npm test in our terminal, which could return something like this if the test failed:

...

1) should equal 5

  AssertionError: expected 4 to equal 5
  + expected - actual
Enter fullscreen mode Exit fullscreen mode

Dynamic Testing

99% of people using tests will not use hard-coded values like this last example, and we would like to dynamically test code written in other files.

Here, we will test to ensure an error WILL be thrown if we aren't signed in. This will not check anything else besides if when the "Authorization" header value is null. We are testing the behavior of the response The basic function we're testing looks like this:

// ./middleware/auth.js

module.exports = (req,res,next) => {
  const auth = req.get('Authorization');
  if (!auth){
    const error = new Error("Authentication Failed");
    throw error;
  }
  next(); 
}
Enter fullscreen mode Exit fullscreen mode

How do we start with this new test? In a testing mindset we can think: How can I make the req.get() method's return value set to null, so we can check if our desired error is sent? We can redefine the method, solely for our test file for when the function runs.

Within a new, auth.js file inside our test folder, we can set a new .get() method for the request object, and add Chai's .expect() to see if we got the desired return value:

// ./test/example.js

const auth = require('../middleware/auth');
const expect = require('chai')(expect);

it('should have an authorization header present', function(){
  const req = {
    get: function() { // defining a new get method for the req object
      return null;
    }
  };
  expect(auth.bind((this, req, {}, () => {})).to.throw('Authentication Failed');
})
Enter fullscreen mode Exit fullscreen mode

Sinon

Say, we were using JWT's, and we import the jwt object. If we wanted a method built into jwt to return null instead of its original function for one of our tests, this will alter the object across all files, which can cause tests to wrongfully fail in some cases.

Instead, we can use what's called a stub, which will save the object's original properties, then you'll execute the function with its new definition, and revert it to the original state at the end of a test.

This will by done with the Sinon package:

npm i --save-dev sinon
Enter fullscreen mode Exit fullscreen mode

Then, in the auth.js file we can define conditions without mutating any of the JWT package:

// ./test/example.js

const jwt = require('jsonwebtoken');
const sinon = require('sinon');

it('...', function(){
  ...
  sinon.stub(jwt, 'sign'); // original sign method saved
  jwt.sign.returns({ hello: "world" });
  jwt.sign.restore(); // original method restored
})
Enter fullscreen mode Exit fullscreen mode

Now, when our test runs the .sign() method built into jwt, it will be replaced to only return the { hello: "world" } object, but its original functionality will be returned immediately after the tests finish.

Organization

Say we want ten tests related to apples and ten related to bananas, how can we easily tell which is for which in a line of 20 tests? We can implement the describe() function from Chai to wrap our tests with a name for their group:

// ./test/example.js

describe('Authentication Middleware', function(){
  it('should equal 5', {
    ...
  })

  it('should not equal 6', {
    ...
  })
});
Enter fullscreen mode Exit fullscreen mode

Now, when you run these tests it will be within a labeled group.

Before/After Keywords

We also get access to two additional function which will come in handy especially if we'd need to sign into a user before we do any defined tests, as well as sign them out after all the tests. Something like this can be done with the before(), which does not allow any code to execute until its own has finished, and after(), executing its code after every test has completed.

It would look like so:

// ./test/example.js

describe('Authentication Middleware', function(){
  before(function() { // executes all lines before any below
    ...
  })

  it('should equal 5', {
    ...
  })

  it('should not equal 6', {
    ...
  })

  after(function(){ // executes after all other code is done
    ...
  })
});
Enter fullscreen mode Exit fullscreen mode

Asynchronous Code

Since JavaScript runs asynchronously, we must tell Mocha to wait for promises to finish or a test could be completed without all the needed information. For this, we can use the done keyword built into Mocha as a parameter for an it() function. This will tell Mocha to wait for the code within a function the done() is inside to execute before finishing the test.

Here is an example of logging into a user account with MongoDB, using the done():

// ./test/example.js

describe('...', function() {
  before(function(done) {
    mongoose
      .connect(
        'mongodb+srv://...'
      )
      .then(result => {
        const user = new User({
          email: 'example@example.com',
          password: 'example',
          name: 'Example',
          posts: [],
          _id: '...'
        });
        return user.save();
      })
      .then(() => {
        done();
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Latest comments (0)