Skip to content

Running Mocha Tests with Webpack in a Browser

June, 2021, craigphicks

Intro

This document describes simple setup for running mocha tests in the browser, using webpack to bundle.

The complete final settings, with both production and development modes integrated, are described in the last section of this document. There is a corresponding repo on github.

The intermediate sections of this document show step by step development of the settings adding one feature at a time. The intermediate sections only use webpack production mode. For webpack production mode, a minimal Express server is used to serve the HTML. Only the in last section is webpack development mode introduced. Webpack development mode uses an integrated Webpack server - so the Express is not used in that case.

Section Directory Structure and setting files describes the directory structure and setting files used for all cases.

Section Using a Single Test File describes settings required for a single self contained mocha test file. Webpack is not required here.

Section Multiple Interdependent Test Files describes settings required when using multiple test files which are interdependent, e.g.. one includes another using import. For this case, webpack is required.

Section Node dependency, tree shaking vs tree pruning describes settings required when a node library, in this example lodash, is imported, but we only want the bundle to include the part(s) of the library we use.

In section Adding Development mode - hot module update and webpack server we enable webpack development mode for the first time. The express server wont be used in development mode because webpack has it's own integrated server for development mode, and that enables hot swapping - which enable auto re-building on source change, so the results are viewable very quickly in the browser.

Directory Structure and Setting Files

top
 |-api
   |-server.ts 
 |-app
   |-resources // permanent files to be copied
     |-favicon.ico
     |-index.html
   |-test-in-js // tsc produced js files
     |-index.js
     |-(test.js)
   |-test-in-ts // test source files
     |-index.ts
     |-(test.ts) // when multiple test files are used 
   |-package.json
   |-tsconfig.json
   |-webpack.mocha.js
 |-static  // distributed files for serving
   |-favicon.ico
   |-index.html
   |-index.js // webpack output bundle file

The typescript transpiler tsc transpiles the .ts source file(s) in top/app/test-in-ts to create the same root name.js file(s) in top/app/test-in-js.

The webpack program bundles the .js files in top/app/test-in-js, as well as any node dependencies if they exist, into the single bundle file top/static/index.js.

The files in top/static/index.js will be served by either the Express server (in production mode), or the webpack integrated server (in development mode).

Note: Webpack has associated auxilliary software to go directly from .ts files to the bundled package, but this document chooses a more basic approach - aiming for transparency.

Using a Single Test File

Express server

api/server.ts

mport express from 'express';
const port = 3080

const app = express();

app.use('/',express.static('../static'))

app.listen(port, '127.0.0.1', () => {
    console.log(`http://localhost:${port}`);
})

The code is available in the repository in the api directory.

To run change dir to top/api and execute

.../api% npm run serve

or

.../api% ts-node server.ts

Assuming the expected files are available in top/static, it will show you the server URL http://localhost:3080 which you can load in a browser.

Application-Test side

app/resource/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Mocha Tests</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="https://unpkg.com/mocha/mocha.css" />
  </head>
  <body>
    <div id="mocha"></div>
    <script src="https://unpkg.com/mocha/mocha.js"></script>
    <script class="mocha-init">
      mocha.setup({ui:'bdd',bail:true});
      mocha.checkLeaks();
    </script>
    <script type='module' src="index.js"></script>
    <script class="mocha-exec">
      mocha.run();
    </script>
  </body>
</html>

This file follows very closely to the one suggested in this mocha documentation. The only change is adding the mocha.setup parameter bail:true. This tells mocha to quit on the first error.

app/test-in-ts/index.ts

export function test(){
  describe('test', () => {
    it('1/3 passes ', () => {
      if (false) throw new Error('fail');
    });
    it('2/3 fails', () => {
      if (true) throw new Error('fail');
    });
    it('3/3 passes', () => {
      if (false) throw new Error('fail');
    });
  });  
}
test()

Because the mocha bail:true parameter is set, test 3/3 will never be reached.

package.json

{
  "name": "app2",
  "version": "1.0.0",
  "main": "src/index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "copy-resources": "mkdir -p ../static && cp  ./resources/* ../static",
    "build": "npm run copy-resources && tsc && cp ./test-in-js/index.js ../static",
    "clean": "rm -rf ../static && rm -rf test-in-js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@types/lodash": "^4.14.170",
    "@types/mocha": "^8.2.2",
    "ts-loader": "^9.2.1",
    "typescript": "^4.2.4",
    "webpack": "^5.37.1",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^3.11.2",
    "webpack-node-externals": "^3.0.0"
  },
  "dependencies": {
    "mocha": "^8.4.0",
    "lodash": "^4.17.21"
  }
}

Note the build script in package.json. The typescript transpiler tsc is called, and the single file of output index.js is copied directly to static. Webpack can be skipped because there is only file index.js and it is suitable recent browsers which can handle es-modules.

The script must be run from the top/app directory:

.../app % npm run build

tsconfig.json

{
  "compilerOptions": {
    "outDir": "./test-in-js/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "allowJs": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports":true
  },
  "include":[
    "test-in-ts/**/*"
  ]
}

Multiple Interdependent Test Files

When multiple test files, where at least one imports another, are used, they must be bundled into one file using a bundler such as Webpack.

webpack.mocha.js

const path = require('path')

module.exports = {
  mode: "production",
  entry: ["./test-in-js"],
  output:{
    filename:'index.js',
    path: path.resolve(__dirname, '../static'),
  }
};

index.ts

import {test} from './test'
let b = true;
b = false;
if (b) test(true); else test(false)

test.js

function throwIfNot(b:boolean){if (!b) throw new Error('fail');}
export function test(b:boolean){
  describe('test', () => {
    it('1/3 passes ', () => {
      throwIfNot(true)
    });
    it('2/3 ???', () => {
      throwIfNot(b)
    });
    it('3/3 passes', () => {
      throwIfNot(true)
    });
  });  
}

The file index.ts imports test.ts. With that condition we cannot simply load multiple files each with their own script tags.

package.json

{
  "name": "app2",
  "version": "1.0.0",
  "main": "src/index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "copy-resources": "mkdir -p ../static && cp  ./resources/* ../static",
    "build": "npm run copy-resources && tsc && webpack --config ./webpack.mocha.js",
    "clean": "rm -rf ../static && rm -rf test-in-js"
  },
  "keywords": [],
  "author": "craigphicks",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@types/lodash": "^4.14.170",
    "@types/mocha": "^8.2.2",
    "mocha": "^8.4.0",
    "ts-loader": "^9.2.1",
    "typescript": "^4.2.4",
    "webpack": "^5.37.1",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^3.11.2",
    "webpack-node-externals": "^3.0.0"
  },
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

Node dependency, tree shaking vs tree pruning

test.js

import * as _ from 'lodash';
function throwIfNot(b:boolean){if (!b) throw new Error('fail ');}
export function test(b:boolean){
  describe('test', () => {
    it('1/4 passes ', () => {
      throwIfNot(true)
    });
    it('2/4 passes', () => {
      const s = _.join(['1','2'],'')  
      throwIfNot(s==='12')
    });
    it('3/3 ???', () => {
      throwIfNot(b)
    });
    it('3/4 passes', () => {
      throwIfNot(true)
    });
  });  
}

The output is big - 77k, because the whole lodash lib is included in the bundle, even though only join is used.

$ ls -al ../static/index.js
-rw-rw-r-- 1 craig craig 71105 Jun  1 15:47 ../static/index.js

Tree pruning (manual) to reduce size

In test.js change import * as _ from 'lodash' to import join from 'lodash/join, and _.join(...) to join(...).

$ ls -al ../static/index.js
-rw-rw-r-- 1 craig craig 772 Jun  1 16:49 ../static/index.js

This manualy pruning limits the bundle size.

However, in the case of lodash we won't do that, because tree shaking is possible.

Tree shaking (automatic) to reduce size

Webpack has the ability to tree-shake, but it only works with es-modules, so libraries (such as lodash) not written as es-modules fail tree shaking.

Fortunately there is an lodash-es version of lodash, written in ES6 format. We replace lodash with lodash-es

npm i lodash-es && npm i -D @types/lodash-es
npm uninstall lodash @types/lodash

and change the import line in test.ts from

import * as _ from 'lodash'

to

import * as _ from 'lodash-es'

Then build

npm run build

and check the size

.../app$ ls -al ../static/index.js
-rw-rw-r-- 1 craig craig 351 Jun  2 07:47 ../static/index.js

The size is small, tree-shaking is working.

Adding Development mode - hot module update and webpack server

Hot module update enables the browser content to be updated automatically as a result of changing any source file, including any typescript test file.

Webpack offers it own built in HTML server for this, so we won't be using express.

The webpack.mocha.config and package.json files are modifed to handle both modes - production and development.

webpack.mocha.config

const path = require('path');

const DIST = path.resolve(__dirname, '../static');
module.exports = (env) => {
  console.log(`env=${JSON.stringify(env, null, 2)}`);
  const isdev = env.dev;
  let ret = {
    mode: isdev ? 'development' : 'production',
    entry: ['./test-in-js'],
    output: {
      filename: 'index.js',
      path: DIST,
    },
  };
  if (isdev) {
    ret = {
      ...ret,
      watch: true,
      devServer: {
        contentBase: DIST,
        watchContentBase: true,
        // proxy: {
        //   '/api': 'http://localhost:3080',
        // },
      },
      devtool: 'inline-source-map',
    };
  }
  console.log('config='+JSON.stringify(ret,null,2))
  return ret
};

package.json

  "name": "app2",
  "version": "1.0.0",
  "main": "src/index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "copy-resources": "mkdir -p ../static && cp  ./resources/* ../static",
    "build": "npm run copy-resources && tsc && webpack --config ./webpack.mocha.js",
    "build-dev": "npm run copy-resources && tsc && ( tsc -w & ) && webpack serve --env dev --config ./webpack.mocha.js --progress",
    "clean": "rm -rf ../static && rm -rf test-in-js"
  },
  "keywords": [],
  "author": "craigphicks",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "@types/lodash-es": "^4.17.4",
    "@types/mocha": "^8.2.2",
    "mocha": "^8.4.0",
    "ts-loader": "^9.2.1",
    "typescript": "^4.2.4",
    "webpack": "^5.37.1",
    "webpack-cli": "^4.7.0",
    "webpack-dev-server": "^3.11.2",
    "webpack-node-externals": "^3.0.0"
  },
  "dependencies": {
    "lodash-es": "^4.17.21"
  }
}

Notice in the script build-dev we see ... && tsc && ( tsc -w & ) && .... This is executing tsc once to completion, then running it again in watch mode in the background, so that any changes are immediately retranspiled into new .js files. webpack serve ... ensures those .js files are being watched to build a new bundle ../static/index.js when the .js files change.

Note: webpack has software to allow going directly from .ts files to bundle, but this document takes a more basic approach for the sake of transparency.

Run in production mode

Build the page to serve

In one window change to top/app

.../app% npm run build

Open the server

In another window change dir to top/api

.../api% npm run serve

or

.../api% ts-node server.ts

It will show you the server URL http://localhost:3080 which you can load in a browser.

Run in development mode

Change to top/app

.../app% npm run build-dev

It will show you the server URL http://localhost:3080 which you can load in a browser.