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.