Streamline Mocha/SuperTest REST tests with async functions

By: (plus.google.com) +David Herron; Date: 2017-11-17 22:53

Tags: Node.js » Asynchronous Programming

Mocha is an excellent unit testing framework for Node.js, and SuperTest is an excellent library for testing REST services. SuperTest is based on SuperAgent which makes it pretty darn easy to write code interfacing with a REST service. The problem with this combination is the test code can get pretty darn caught up in boilerplate callback functions, if you use Mocha and SuperTest the way its documented. Fortunately it's pretty easy to make SuperTest return a Promise, which then plays well with async functions, and Mocha is easily able to use an async function in a test scenario. So let's get started.

To review the structure of a Mocha test suite, it goes something like this:

const assert = require('chai').assert;
describe('Test Suite Name', function() {
  describe('Test Area', function() {
    it('should have behavior #1', function(done) {
      assert.equal(testedValue, expectedValue);
      done();
    });
    it('should have behavior #2', function(done) {
      assert.equal(testedValue, expectedValue);
      done();
    });
    it('should have behavior #3', function(done) {
      assert.equal(testedValue, expectedValue);
      done();
    });
  });
});

Tests are run using the Mocha command as so:

$ mocha test-suite.js

This example uses Chai Assertions because they're very nice and useful.

Mocha as used above is okay when the tested code is synchronous and things are straight-forward. But I've found while writing test code for the REST service I'm creating, that things can become quite convoluted if you use the callback-oriented test framework, and the callback-oriented usage of SuperTest.

That was a long sentence, and an example should help instead:

describe('Weather Profile', function() {

    let pvSystem;
    before(function(done) {
        request(appurl).post('/systems')
        .set('Content-Type', 'application/json')
        .set('Acccept', 'application/json')
        .timeout({ response: 100000, deadline: 100000 })
        .send(require('./data/systems/system-1.js'))
        .then(results => { 
            
            if (results.error !== null 
                && typeof results.error !== 'undefined' 
                && results.error !== false) {
                console.log(util.inspect(results.error));
            }
            assert.ok(results.status === 200);
            pvSystem = results.body;
            // console.log(util.inspect(pvSystem));
            utilsystem.checkSystem1(pvSystem);
            done();
        })
        .catch(err => { 
            console.error(err);
            done(err); 
        });
    });

    var weatherID0;
    var weatherID1;
    var weatherID2;
    it('should retrieve all Weather Profiles from System', function(done) {
        request(appurl).get(`/systems/${pvSystem.identifier}/weathers`)
        .timeout({ response: 100000, deadline: 100000 })
        .end(function(err, response) {
            if (err) throw err;
            if (response.error !== null 
                && typeof response.error !== 'undefined' 
                && response.error !== false) {
                console.log(util.inspect(response.error));
            }
            assert.ok(response.status === 200);
            // console.log(util.inspect(response.body));
            assert.equal(response.body.length, 1);
            utilweather.checkWeatherProfile1(response.body[0]);
            weatherID0 = response.body[0].measurementID;
            done();
        });
    });
});

Here's an example from the tests I've written -- it's not too bad, but there's still some callback boilerplate. This test suite contains some more convoluted code that's even worse. Overall we can improve this considerably by using async functions with the await keyword to wait on promises from the SuperTest library.

The test suite follows a pattern that may be common practice. In a before hook it pushes a test object into the database, and then runs a few manipulations of that object. The pvSystem object is semi-global within this set of tests, so we can easily refer to it in every test. In particular we need its ID to pass as a parameter to further REST calls. The weatherID0/1/2 objects are used similarly.

First, Mocha can use an async function as so:

before(async function() {
    ...
});

The code within that async function can then use the await keyword, thrown exceptions are automatically caught by the async function, and a number of other good things.

let pvSystem;
before(async function() {
    let results = utiltest.checkResponse(
        await request(appurl).post('/systems')
        .set('Content-Type', 'application/json')
        .set('Acccept', 'application/json')
        .timeout({ response: 100000, deadline: 100000 })
        .send(require('./data/systems/system-1.js'))
        .catch(err => { 
            console.error(err);
            throw err; 
        })
    );
    assert.ok(results.status === 200);
    pvSystem = results.body;
    // console.log(util.inspect(pvSystem));
    utilsystem.checkSystem1(pvSystem);
});

Isn't this much easier to read? But, what happened?

Instead of function(done) { ... } there is no done argument, which causes Mocha to expect to receive a Promise, and the async keyword is used, which signifies among other things that the function will return a Promise.

First off I defined a module, utiltest.js, containing this:

const util = require('util');

module.exports.checkResponse = function(response) {
    if (response.error !== null 
        && typeof response.error !== 'undefined' 
        && response.error !== false) {
        console.log(util.inspect(response.error));
    }
    return response;
};

That one test appears all over the place in the test suite, so it's best to localize the code to one place. We simply require that into every test script, and call it as utiltest.checkResponse(...) as shown here. It returns the object it's given so it's easy to use in-line.

Next, we call the request function in a model which causes it to return a Promise. That is - SuperAgent builds on the SuperTest API, so the best source for SuperAgent documentation is SuperTest. In the SuperTest documentation it says that if a request thing does not include a .end call, it will instead return a Promise which can be used with a .then. See here: (visionmedia.github.io) http://visionmedia.github.io/superagent/#request-basics

That's nice, except the .then is still buried inside a callback. Better is to use this within an async function and to use the await keyword. That's what we've done here.

The await keyword then causes the value from the request to be returned as its value. So what would have been this:

request(appurl).post('/systems')
    .set('Content-Type', 'application/json')
    .set('Acccept', 'application/json')
    .timeout({ response: 100000, deadline: 100000 })
    .send(require('./data/systems/system-1.js'))
    .then(results => {
        handle results here
    })
    .catch(err => { 
        console.error(err);
        done(err); 
    })

Is now much easier to deal with and you don't have nested callback functions.

The entire new-and-improved code reads as so:

describe('Weather Profile', function() {

    let pvSystem;
    before(async function() {
        let results = utiltest.checkResponse(
            await request(appurl).post('/systems')
            .set('Content-Type', 'application/json')
            .set('Acccept', 'application/json')
            .timeout({ response: 100000, deadline: 100000 })
            .send(require('./data/systems/system-1.js'))
            .catch(err => { 
                console.error(err);
                throw err; 
            })
        );
        assert.ok(results.status === 200);
        pvSystem = results.body;
        // console.log(util.inspect(pvSystem));
        utilsystem.checkSystem1(pvSystem);
    });

    var weatherID0;
    var weatherID1;
    var weatherID2;
    it('should retrieve all Weather Profiles from System', async function() {
        let response = utiltest.checkResponse(
            await request(appurl).get(`/systems/${pvSystem.identifier}/weathers`)
                .timeout({ response: 100000, deadline: 100000 })
                .catch(err => { 
                    console.error(err);
                    throw err; 
                })
        );
                
        assert.ok(response.status === 200);
        // console.log(util.inspect(response.body));
        assert.equal(response.body.length, 1);
        utilweather.checkWeatherProfile1(response.body[0]);
        weatherID0 = response.body[0].measurementID;
    });
});

Notice that this still uses a .catch block. That's so we get an output of any thrown errors to aid the programmer in diagnosing test failures.

« npm version 5 has major usability bug with installing packages locally Node.js toolkit for mobile iOS and Android devices announced by Janea Systems »
2016 Election 2018 Elections Acer C720 Ad block Air Filters Air Quality Air Quality Monitoring AkashaCMS Amazon Amazon Kindle Amazon Web Services America Amiga and Jon Pertwee Android Anti-Fascism AntiVirus Software Apple Apple Hardware History Apple iPhone Apple iPhone Hardware April 1st Arduino ARM Compilation Artificial Intelligence Astronomy Astrophotography Asynchronous Programming Authoritarianism Automated Social Posting AWS DynamoDB AWS Lambda Ayo.JS Bells Law Big Brother Big Data Big Finish Big Science Bitcoin Mining Black Holes Blade Runner Blockchain Blogger Blogging Books Botnets Cassette Tapes Cellphones China China Manufacturing Christopher Eccleston Chrome Chrome Apps Chromebook Chromebox ChromeOS CIA CitiCards Citizen Journalism Civil Liberties Climate Change Clinton Cluster Computing Command Line Tools Comment Systems Computer Accessories Computer Hardware Computer Repair Computers Conservatives Cross Compilation Crouton Cryptocurrency Curiosity Rover Currencies Cyber Security Cybermen Cybersecurity Daleks Darth Vader Data backup Data Formats Data Storage Database Database Backup Databases David Tenant DDoS Botnet Department of Defense Department of Justice Detect Adblocker Developers Editors Digital Nomad Digital Photography Diskless Booting Disqus DIY DIY Repair DNP3 Do it yourself Docker Docker MAMP Docker Swarm Doctor Who Doctor Who Paradox Doctor Who Review Drobo Drupal Drupal Themes DVD E-Books E-Readers Early Computers eGPU Election Hacks Electric Bicycles Electric Vehicles Electron Eliminating Jobs for Human Emdebian Encabulators Energy Efficiency Enterprise Node EPUB ESP8266 Ethical Curation Eurovision Event Driven Asynchronous Express Face Recognition Facebook Fake News Fedora VirtualBox Fifth Doctor File transfer without iTunes FireFly Flash Flickr Fraud Freedom of Speech Front-end Development G Suite Gallifrey Gig Economy git Github GitKraken Gitlab GMAIL Google Google Chrome Google Gnome Google+ Government Spying Great Britain Green Transportation Hate Speech Heat Loss Hibernate High Technology Hoax Science Home Automation HTTP Security HTTPS Human ID I2C Protocol Image Analysis Image Conversion Image Processing ImageMagick In-memory Computing InfluxDB Infrared Thermometers Insulation Internet Internet Advertising Internet Law Internet of Things Internet Policy Internet Privacy iOS iOS Devices iPad iPhone iPhone hacking Iron Man iShowU Audio Capture iTunes Janet Fielding Java JavaFX JavaScript JavaScript Injection JDBC John Simms Journalism Joyent Kaspersky Labs Kext Kindle Kindle Marketplace Large Hadron Collider Lets Encrypt LibreOffice Linux Linux Hints Linux Single Board Computers Logging Mac Mini Mac OS Mac OS X MacBook Pro Machine Learning Machine Readable ID Macintosh macOS macOS High Sierra macOS Kext MacOS X setup Make Money Online Make Money with Gigs March For Our Lives MariaDB Mars Mass Violence Matt Lucas MEADS Anti-Missile Mercurial MERN Stack Michele Gomez Micro Apartments Microsoft Military AI Military Hardware Minification Minimized CSS Minimized HTML Minimized JavaScript Missy Mobile Applications Mobile Computers MODBUS Mondas Monetary System MongoDB Mongoose Monty Python MQTT Music Player Music Streaming MySQL NanoPi Nardole NASA Net Neutrality Network Attached Storage Node Web Development Node.js Node.js Database Node.js Performance Node.js Testing Node.JS Web Development Node.x North Korea npm NVIDIA NY Times Online advertising Online Community Online Fraud Online Journalism Online Photography Online Video Open Media Vault Open Source Open Source and Patents Open Source Governance Open Source Licenses Open Source Software OpenAPI OpenJDK OpenVPN Palmtop PDA Patrick Troughton PayPal Paywalls Personal Flight Peter Capaldi Peter Davison Phishing Photography PHP Plex Plex Media Server Political Protest Politics Postal Service Power Control President Trump Privacy Private E-mail server Production use Public Violence Raspberry Pi Raspberry Pi 3 Raspberry Pi Zero ReactJS Recaptcha Recycling Refurbished Computers Remote Desktop Removable Storage Republicans Retro Computing Retro-Technology Reviews RFID Rich Internet Applications Right to Repair River Song Robotics Robots Rocket Ships RSS News Readers rsync Russia Russia Troll Factory Russian Hacking Rust SCADA Scheme Science Fiction SD Cards Search Engine Ranking Season 1 Season 10 Season 11 Security Security Cameras Server-side JavaScript Serverless Framework Servers Shell Scripts Silence Simsimi Skype SmugMug Social Media Social Media Networks Social Media Warfare Social Network Management Social Networks Software Development Software Patents Space Flight Space Ship Reuse Space Ships SpaceX Spear Phishing Spring Spring Boot Spy Satellites SQLite3 SSD Drives SSD upgrade SSH SSH Key SSL Stand For Truth Strange Parts Swagger Synchronizing Files Tegan Jovanka Telescopes Terrorism The Cybermen The Daleks The Master Time-Series Database Tom Baker Torchwood Total Information Awareness Trump Trump Administration Trump Campaign Twitter Ubuntu Udemy UDOO US Department of Defense Video editing Virtual Private Networks VirtualBox VLC VNC VOIP Vue.js Walmart Weapons Systems Web Applications Web Developer Resources Web Development Web Development Tools Web Marketing Webpack Website Advertising Weeping Angels WhatsApp William Hartnell Window Insulation Windows Windows Alternatives Wordpress World Wide Web Yahoo YouTube YouTube Monetization