Browser Testing with PhantomJS and Mocha – Part 1

If you have been following along for the past couple of weeks, you will know that I’ve been writing a browser library recently. I’m writing the library in ES2015 and then transpiling it into UMD.

A sidebar on bugs in BabelJS
I did bump into a bug when transpiling into the UMD module format. The bug is pretty much across the module transforms, and manifests as a ‘Maximum Call Stack Exceeded’ error with _typeof. The bug is T6777. There is a workaround, which is to add a typeof undefined; line at the top of your library.

Back to the problem at hand. I’ve already used Mocha to test my library and I use mocks to attempt to exercise the code, but at some point you have to run it in a browser. There are two steps to this. The first is to set up a test system that runs in a browser, and the second is to run the test system through a headless browser so it can be automated. Let’s tackle the first step today.

My library is a client library to access a remote AJAX environment. I want the library to use either a provided URL or the URL the page was loaded from – whichever is appropriate. As a result, I need to load the files over the Internet – loading from a file:// URL isn’t good enough. To handle this, I’m going to:

  • Create a local test server
  • Load the files into a static service area
  • Run the pages in a browser

To this end, I’ve got a Gulp task that builds my server:

var gulp = require('gulp'),
    babel = require('gulp-babel'),
    concat = require('gulp-concat'),
    sourcemaps = require('gulp-sourcemaps'),
    config = require('../configuration');

module.exports = exports = function() {
    return gulp.src(config.source.files)
        .pipe(sourcemaps.init())
        .pipe(concat('MyLibrary.js'))
        .pipe(babel())
        .pipe(sourcemaps.write('.'))
        .pipe(gulp.dest(config.destination.directory));
};

I store my gulp tasks in a separate file – one file per task. I then require the file in the main Gulpfile.js:

var gulp = require('gulp');

gulp.task('build', require('./gulp/tasks/build'));

I now have a MyLibrary.js file and a MyLibrary.js.map file in the dist directory. Building the server area is just as easy:

var gulp = require('gulp'),
    config = require('../configuration');

// Builds the server.rootdir up to service test files
module.exports = exports = function() {
    return gulp.src(config.test.server.files)
        .pipe(gulp.dest(config.test.server.rootdir));
};

My configuration.js exposes a list of files like this:

module.exports = exports = {
    source: {
        files: [ 'src/**/*.js' ]
    },
    destination: {
        directory: 'dist'
    },
    test: {
        mocha: [ 'test/**/*.js' ],
        server: {
            files: [
                'browser-tests/global.html',
                'browser-tests/global-tests.js',
                'dist/MyLibrary.js',
                'dist/MyLibrary.js.map',
                'node_modules/chai/chai.js',
                'node_modules/mocha/mocha.css',
                'node_modules/mocha/mocha.js'
            ],
            port: 3000,
            rootdir: 'www'
        }
    }
};

Take a look at the test.server.files object. That contains three distinct sections – the browser test files (more on those in a moment), the library files under test and the testing libraries. You should already have these installed, but if you don’t, you can install them:

npm install --save-dev mocha chai

I will have a www directory with all the files I need in it once I run the gulp buildserver command. Next, I need a server. I use ExpressJS for this. First off, install ExpressJS:

npm install --save-dev express

Note that this is a dev install – not a production install, hence the use of the --save-dev tag. I want express listed in devDependencies. Now, on to the server code, which I place in testserver.js:

var express = require('express'),
    config = require('./gulp/configuration');

var app = express();
app.use(express.static(config.test.server.rootdir));
app.listen(config.test.server.port || 3000, function() {
    console.info('Listening for connections');
});

This is about the most basic configuration for an ExpressJS server you can get. I’m serving static pages from the area I’ve built. That’s enough of infrastructure – now, how about running tests? I’ve got two files in my files list that I have not written yet. The first is a test file called global-tests.js and the other is a HTML file that sets up the test run – called global.html. The global-tests.js is a pretty normal Mocha test suite:

/* global describe, it, chai, MyLibrary */
var expect = chai.expect;

describe('MyLibrary.Client - Global Browser Object', function () {
    it('should have an MyLibrary global object', function () {
        expect(MyLibrary).to.be.a('object');
    });

    it('should have an MyLibrary.Client method', function () {
        expect(MyLibrary.Client).to.be.a('function');
    });

    it('should create a Client object when run in a browser', function () {
        var client = new MyLibrary.Client();
        expect(client).to.be.an.instanceof(MyLibrary.Client);
    });

    it('should set the url appropriately', function () {
        var client = new MyLibrary.Client();
        expect(client.url).to.equal('http://localhost:3000');
    });

    it('should set the environment appropriately', function () {
        var client = new MyLibrary.Client();
        expect(client.environment).to.equal('web/globals');
    });
});

There are a couple of changes. Firstly, this code is going to run in the browser, so you must write your tests for that environment. Secondly, it expects that the test framework is established already – it expects the chai library to be pre-loaded. One other thing is that this is a minimal test load. The majority of the testing is done inside my standard Mocha test run. As long as you have your tests exercise all paths within the code across the test suites (both the standard mocha tests and the browser tests), then you will be ok. I only test things that need the browser in order to test them.

The global.html test file sets up the tests, loads the appropriate libraries and then executes the tests:

<!DOCTYPE html>
<html>

<head>
    <title>Mocha Test File: Global Library Definition</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="mocha.css">
</head>

<body>
    <div id="mocha"></div>
    <script src="mocha.js"></script>
    <script src="chai.js"></script>
    <script>
        mocha.setup('bdd');
        mocha.reporter('html');
    </script>
    <script src="MyLibrary.js"></script>
    <script src="global-tests.js"></script>
    <script>
        mocha.run();
    </script>
</body>

</html>

I’m intending on writing a test file that implements the global object version, AMD module definition and browserify to ensure that the library runs in all environments. Each environment will have it’s own HTML file and test suite file. I can include as many of these sets as I want.

Running the tests

Running the tests at this stage is a two-step process. First, you start the server:

node testserver.js

Secondly, you browse to http://localhost:3000/global.html – note the initiator for your test suite is the HTML file. If you have done everything properly, the tests will just work:

mocha-browser

If things don’t work, you can use Developer Tools to figure out what is going on and correct the problem, then re-run the tests. Since this is an ES2015 project, there are some things that may require a polyfill. You can provide your own (mine only needs a polyfill for Object.assign – a matter for a couple of dozen lines of code), or you can use a major ES2015 polyfill like core.js – just ensure you load the polyfill in your test environment. This is also a great pointer to ensure your library has the right dependencies listed and that you have documented your requirements for the browser.

In the next article (Happy New Year!) I will integrate this into automated testing so that you don’t have to open a browser to do this task.

An ECMAScript 6, CommonJS and RequireJS Project

I’ve been writing a lot of CommonJS code recently – the sort that you would include in Node projects on the server side. I’ve recently had a thought that I would like to do a browser-side project. However, how do you produce a browser library that can be consumed by everyone?

The different styles of modules

Let’s say I have a class Client(). If I were operating in Node or Browserify, I’d do something like this:

var Client = require('my-client-package');

var myclient = new Client();

This is called CommonJS format. I like it – it’s nice and clean. However, that’s not the only way to potentially consume the library. You can also bring it in with RequireJS:

define(['Client'], function(Client) {
    var myclient = new Client();

});

Finally, you could also register the variable as a global and bring it in with a script HTML tag:

<script src="node_modules/my-client-package/index.js"></script>
<script>
    var client = new Client();
</script>

You can find a really good writeup of the differences between CommonJS and AMD in an article by Addy Osmani.

Three different techniques. If we were being honest, they are all valid and have their place, although you might have your favorite technique. As a library developer, I want to support the widest range of JavaScript developers which means supporting three different styles of code. This brings me to UMD format. I named it “Ugly Module Definition”, and you can see why when you look at the code:

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['b'], function (b) {
            return (root.returnExportsGlobal = factory(b));
        });
    } else if (typeof module === 'object' && module.exports) {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like enviroments that support module.exports,
        // like Node.
        module.exports = factory(require('b'));
    } else {
        // Browser globals
        root.returnExportsGlobal = factory(root.b);
    }
}(this, function (b) {
    // Use b in some fashion

    return {// Your exported interface };
}));

Seriously, could this code be any uglier? I like writing my code in ECMAScript 2015, also known as ES6. So, can I write a class in ES6 and then transpile it to the right format? Further, can I set up a project that has everything I need to test the library? It turns out I can. Here is how I did it.

Project Setup

These days, I tend to create a directory for my project, put some stuff in it and then push it up to a newly created GitHub repository. I’m going to assume you have already created a GitHub user and then created a GitHub repository called ‘my-project’. Let’s get started:

mkdir my-project
cd my-project
git init
git remote add origin https://github.com/myuser/my-project
npm init --yes
git add package.json
git commit -m "First Commit"
git push -u origin master

Perhaps unshockingly, I have a PowerShell script for this functionality since I do it so often. All I have to do is remember to check in things along the way now and push the repository to GitHub at the end of my work.

My Code

I keep my code in the src directory, The tests are in the test directory. The distribution file is in the dist directory. Let’s start with looking at my src/Client.js code:

export default class Client {
    constructor(options = {}) {
    }
}

Pretty simple, right? The point of this is not to concentrate on code – it’s about the build process. I’ve also got a test in the test/Client.js file:

/* global describe, it */

// Testing Library Functions
import { expect } from 'chai';

// Objects under test
import Client from '../src/Client';

describe('Client.js', () => {
    describe('constructor', () => {
        it('should return a Client object', () => {
            let client = new Client();
            expect(client).to.be.instanceof(Client);
        });
    });
});

I like to use Mocha and Chai for my tests, so this is written with that combination in mind. Note the global comment on the first line – that prevents Visual Studio Code from putting green squiggles underneath the mocha globals.

Build Modules

I decided some time along the way that I won’t use gulp or grunt unless I have to. In this case, I don’t have to. My toolset includes:

Let’s take a look at my package.json:

{
    "name": "my-project",
    "version": "0.1.0",
    "description": "A client library written in ES6",
    "main": "dist/Client.js",
    "scripts": {
        "pretest": "eslint src test",
        "test": "mocha",
        "build": "babel src --out-file dist/Client.js --source-maps"
    },
    "keywords": [
    ],
    "author": "Adrian Hall <adrian@shellmonger.com>",
    "license": "MIT",
    "devDependencies": {
        "babel-cli": "^6.3.17",
        "babel-plugin-transform-es2015-modules-umd": "^6.3.13",
        "babel-preset-es2015": "^6.3.13",
        "babel-register": "^6.3.13",
        "chai": "^3.4.1",
        "eslint": "^1.10.3",
        "mocha": "^2.3.4"
    },
    "babel": {
        "presets": [
            "es2015"
        ],
        "plugins": [
            "transform-es2015-modules-umd"
        ]
    }
}

A couple of regions need to be discussed here. Firstly, I’ve got two basic npm commands I can run:

  • npm test will run the tests
  • npm run build will build the client library

I’ve got a bunch of devDependencies to implement this build system. Also note the “babel” section – this is what would normally go in the .babelrc – you can also place it in your package.json file.

The real secret sauce here is the build script. This uses a module transform to create a UMD format library from your ES6 code. You don’t even have to worry about reading that ES5 code – it’s ugly, but it works.

Editor Files

I use Visual Studio Code, so I need a jsconfig.json file in the root of my project:

{
    "compilerOptions": {
        "target": "ES6"
    }
}

This tells Visual Studio Code to use ES6 syntax. I’m hopeful the necessity of this will go away soon. I’m hoping that I’m not the only one who is contributing to this repository. Collaboration is great, but you want to set things up so that people coming newly in to the project can get started with your coding style straight away. I include a .editorconfig file as well:

root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.json]
insert_final_newline = false

You can read about editorconfig files on their site. This file is used by a wide variety of editors – if your editor is on the list, you should also install the plugin.

ESLint Configuration

I have a .eslintrc.js file at the root of the project. I’ve got that in a gist since it is so big and I just cut and paste it into the root directory.

Test Configuration

My test directory is different – it expects to operate within mocha, so I need an override to tell eslint that this is all about mocha. Here is the test/.eslintrc file:

module.exports = exports = {
    "env": {
        "es6": true,
        "mocha": true
    }
};

I also need a mocha.opts file to tell mocha that the tests are written in ES6 format:

--compilers js:babel-register

Wrapping up

You will need a dist directory. I place a README.md file in there that describes the three use cases for the library – CommonJS, AMD and globals. That README.md file is really only there to ensure the dist directory exists when you clone the repository.

I also need to add a README.md at the root of the project. It’s required if I intend to publish the project to the NPM repository. Basic instructions on how to install and use the library is de rigeur, but you can put whatever you want in there in reality.

I have not addressed jsdoc yet – you should be doing it in your source files, and it should be a postbuild step in your package.json file.

You can now run the tests and build through the npm commands and get a library that can be used across the board.