We recently wrote about unit testing Cloudflare Workers within a mock environment using CloudWorker (a Node.js based mock Cloudflare Worker environment created by Dollar Shave Club's engineering team). See Unit Testing Worker Functions.
Even though Cloudflare Workers deploy globally within seconds, software developers often choose to use local mock environments to have the fastest possible feedback loop while developing on their local machines. CloudWorker is perfect for this use case but as it is still a mock environment it does not guarantee an identical runtime or environment with all Cloudflare Worker APIs and features. This gap can make developers uneasy as they do not have 100% certainty that their tests will succeed in the production environment.
In this post, we're going to demonstrate how to generate a Cloudflare Worker compatible test harness which can execute mocha unit tests directly in the production Cloudflare environment.
Directory Setup
Create a new folder for your project, change it to your working directory and run npm init
to initialise the package.json
file.
Run mkdir -p src && mkdir -p test/lib && mkdir dist
to create folders used by the next steps. Your folder should look like this:
.
./dist
./src/worker.js
./test
./test/lib
./package.json
npm install --save-dev mocha exports-loader webpack webpack-cli
This will install Mocha (the unit testing framework), Webpack (a tool used to package the code into a single Worker script) and Exports Loader (a tool used by Webpack to import the Worker script into the Worker based Mocha environment.
npm install --save-dev git+https://github.com/obezuk/mocha-loader.git
This will install a modified version of Webpack's mocha loader. It has been modified to support the Web Worker environment type. We are excited to see Web Worker support merged into Mocha Loader so please vote for our pull request here: https://github.com/webpack-contrib/mocha-loader/pull/77
Example Script
Create your Worker script in ./src/worker.js
:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function addition(a, b) {
return a + b
}
async function handleRequest(request) {
const added = await addition(1,3)
return new Response(`The Sum is ${added}!`)
}
Add Tests
Create your unit tests in ./test/test.test.js
:
const assert = require('assert')
describe('Worker Test', function() {
it('returns a body that says The Sum is 4', async function () {
let url = new URL('https://worker.example.com')
let req = new Request(url)
let res = await handleRequest(req)
let body = await res.text()
assert.equal(body, 'The Sum is 4!')
})
it('does addition properly', async function() {
let res = await addition(1, 1)
assert.equal(res, 2)
})
})
Mocha in Worker Test Harness
In order to execute mocha and unit tests within Cloudflare Workers we are going to build a Test Harness. The Test Harness script looks a lot like a normal Worker script but integrates your ./src/worker.js
and ./test/test.test.js
into a script which is capable of executing the Mocha unit tests within the Cloudflare Worker runtime.
Create the below script in ./test/lib/serviceworker-mocha-harness.js
.
import 'mocha';
import 'mocha-loader!../test.test.js';
var testResults;
async function mochaRun() {
return new Promise(function (accept, reject) {
var runner = mocha.run(function () {
testResults = runner.testResults;
accept();
});
});
}
addEventListener('fetch', event => {
event.respondWith(handleMochaRequest(event.request))
});
async function handleMochaRequest(request) {
if (!testResults) {
await mochaRun();
}
var headers = new Headers({
"content-type": "application/json"
})
var statusCode = 200;
if (testResults.failures != 0) {
statusCode = 500;
}
return new Response(JSON.stringify(testResults), {
"status": statusCode,
"headers": headers
});
}
Object.assign(global, require('exports-loader?handleRequest,addition!../../src/worker.js'));
Mocha Webpack Configuration
Create a new file in the project root directory called: ./webpack.mocha.config.js
. This file is used by Webpack to bundle the test harness, worker script and unit tests into a single script that can be deployed to Cloudflare.
module.exports = {
target: 'webworker',
entry: "./test/lib/serviceworker-mocha-harness.js",
mode: "development",
optimization: {
minimize: false
},
performance: {
hints: false
},
node: {
fs: 'empty'
},
module: {
exprContextCritical: false
},
output: {
path: __dirname + "/dist",
publicPath: "dist",
filename: "worker-mocha-harness.js"
}
};
Your file structure should look like (excluding node_modules):
.
./dist
./src/worker.js
./test/test.test.js
./test/lib/serviceworker-mocha-harness.js
./package.json
./package-lock.json
./webpack.mocha.config.js
Customising the test harness.
If you wish to extend the test harness to support your own test files you will need to add additional test imports to the top of the script:
import 'mocha-loader!/* TEST FILE NAME HERE */'
If you wish to import additional functions from your Worker script into the test harness environment you will need to add them comma separated into the last line:
Object.assign(global, require('exports-loader?/* COMMA SEPARATED FUNCTION NAMES HERE */!../../src/worker.js'));
Running the test harness
Deploying and running the test harness is identical to deploying any other Worker script with Webpack.
Modify the scripts section of package.json to include the build-harness command.
"scripts": {
"build-harness": "webpack --config webpack.mocha.config.js -p --progress --colors"
}
In the project root directory run the command npm run build-harness
to generate and bundle your Worker script, Mocha and your unit tests into ./dist/worker-mocha-harness.js
.
Upload this script to a test Cloudflare workers route and run curl --fail https://test.example.org
. If the unit tests are successful it will return a 200
response, and if the unit tests fail a 500
response.
Integrating into an existing CI/CD pipeline
You can integrate Cloudflare Workers and the test harness into your existing CI/CD pipeline by using our API: https://developers.cloudflare.com/workers/api/.
The test harness returns detailed test reports in JSON format:
Example Success Response
{
"stats": {
"suites": 1,
"tests": 2,
"passes": 2,
"pending": 0,
"failures": 0,
"start": "2019-04-23T06:24:33.492Z",
"end": "2019-04-23T06:24:33.590Z",
"duration": 98
},
"tests": [
{
"title": "returns a body that says The Sum is 4",
"fullTitle": "Worker Test returns a body that says The Sum is 4",
"duration": 0,
"currentRetry": 0,
"err": {}
},
{
"title": "does addition properly",
"fullTitle": "Worker Test does addition properly",
"duration": 0,
"currentRetry": 0,
"err": {}
}
],
"pending": [],
"failures": [],
"passes": [
{
"title": "returns a body that says The Sum is 4",
"fullTitle": "Worker Test returns a body that says The Sum is 4",
"duration": 0,
"currentRetry": 0,
"err": {}
},
{
"title": "does addition properly",
"fullTitle": "Worker Test does addition properly",
"duration": 0,
"currentRetry": 0,
"err": {}
}
]
}
Example Failure Response
{
"stats": {
"suites": 1,
"tests": 2,
"passes": 0,
"pending": 0,
"failures": 2,
"start": "2019-04-23T06:25:52.100Z",
"end": "2019-04-23T06:25:52.170Z",
"duration": 70
},
"tests": [
{
"title": "returns a body that says The Sum is 4",
"fullTitle": "Worker Test returns a body that says The Sum is 4",
"duration": 0,
"currentRetry": 0,
"err": {
"name": "AssertionError",
"actual": "The Sum is 5!",
"expected": "The Sum is 4!",
"operator": "==",
"message": "'The Sum is 5!' == 'The Sum is 4!'",
"generatedMessage": true,
"stack": "AssertionError: 'The Sum is 5!' == 'The Sum is 4!'\n at Context.<anonymous> (worker.js:19152:16)"
}
},
{
"title": "does addition properly",
"fullTitle": "Worker Test does addition properly",
"duration": 0,
"currentRetry": 0,
"err": {
"name": "AssertionError",
"actual": "3",
"expected": "2",
"operator": "==",
"message": "3 == 2",
"generatedMessage": true,
"stack": "AssertionError: 3 == 2\n at Context.<anonymous> (worker.js:19157:16)"
}
}
],
"pending": [],
"failures": [
{
"title": "returns a body that says The Sum is 4",
"fullTitle": "Worker Test returns a body that says The Sum is 4",
"duration": 0,
"currentRetry": 0,
"err": {
"name": "AssertionError",
"actual": "The Sum is 5!",
"expected": "The Sum is 4!",
"operator": "==",
"message": "'The Sum is 5!' == 'The Sum is 4!'",
"generatedMessage": true,
"stack": "AssertionError: 'The Sum is 5!' == 'The Sum is 4!'\n at Context.<anonymous> (worker.js:19152:16)"
}
},
{
"title": "does addition properly",
"fullTitle": "Worker Test does addition properly",
"duration": 0,
"currentRetry": 0,
"err": {
"name": "AssertionError",
"actual": "3",
"expected": "2",
"operator": "==",
"message": "3 == 2",
"generatedMessage": true,
"stack": "AssertionError: 3 == 2\n at Context.<anonymous> (worker.js:19157:16)"
}
}
],
"passes": []
}
This is really powerful and can allow you to execute your unit tests directly in the Cloudflare runtime, giving you more confidence before releasing your code into production. We hope this was useful and welcome any feedback.