Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon

Testing WebAssembly modules with Jest [Tutorial]

Save for later
  • 7 min read
  • 15 Oct 2018

article-image

WebAssembly (Wasm) represents an important stepping stone for the web platform. Enabling a developer to run compiled code on the web without a plugin or browser lock-in presents many new opportunities.

This article is taken from the book Learn WebAssembly by Mike Rourke. This book will introduce you t powerful WebAssembly concepts that will help you write lean and powerful web applications with native performance.

Well-tested code prevents regression bugs, simplifies refactoring, and alleviates some of the frustrations that go along with adding new features. Once you've compiled a WebAssembly module, you should write tests to ensure it's functioning as expected, even if you've written tests for C, C++, or Rust code you compiled it from.

In this tutorial, we'll use Jest, a JavaScript testing framework, to test the functions in a compiled Wasm module.


The code being tested

All of the code used in this example is located on GitHub. The code and corresponding tests are very simple and are not representative of real-world applications, but they're intended to demonstrate how to use Jest for testing. The following code represents the file structure of the /testing-example folder:


├── /src
|    ├── /__tests__
|    │    └── main.test.js
|    └── main.c
├── package.json
└── package-lock.json

The contents of the C file that we'll test, /src/main.c, is shown as follows:


int addTwoNumbers(int leftValue, int rightValue) {
    return leftValue + rightValue;
}


float divideTwoNumbers(float leftValue, float rightValue) {
return leftValue / rightValue;
}

double findFactorial(float value) {
int i;
double factorial = 1;

for (i = 1; i <= value; i++) {
factorial = factorial * i;
}
return factorial;
}

All three functions in the file are performing simple mathematical operations. The package.json file includes a script to compile the C file to a Wasm file for testing. Run the following command to compile the C file:


npm run build

There should be a file named main.wasm in the /src directory. Let's move on to describing the testing configuration step.


Testing configuration

The only dependency we'll use for this example is Jest, a JavaScript testing framework built by Facebook. Jest is an excellent choice for testing because it includes most of the features you'll need out of the box, such as coverage, assertions, and mocking. In most cases, you can use it with zero configuration, depending on the complexity of your application. If you're interested in learning more, check out Jest's website at https://jestjs.io. Open a terminal instance in the /chapter-09-node/testing-example folder and run the following command to install Jest:


npm install

In the package.json file, there are three entries in the scripts section: build, pretest, and test. The build script executes the emcc command with the required flags to compile /src/main.c to /src/main.wasm. The test script executes the jest command with the --verbose flag, which provides additional details for each of the test suites. The pretest script simply runs the build script to ensure /src/main.wasm exists prior to running any tests.


Tests file review

Let's walk through the test file, located at /src/__tests__/main.test.js, and review the purpose of each section of code. The first section of the test file instantiates the main.wasm file and assigns the result to the local wasmInstance variable:


const fs = require('fs');
const path = require('path');


describe('main.wasm Tests', () => {
let wasmInstance;

beforeAll(async () => {
const wasmPath = path.resolve(__dirname, '..', 'main.wasm');
const buffer = fs.readFileSync(wasmPath);
const results = await WebAssembly.instantiate(buffer, {
env: {
memoryBase: 0,
tableBase: 0,
memory: new WebAssembly.Memory({ initial: 1024 }),
table: new WebAssembly.Table({ initial: 16, element: 'anyfunc' }),
abort: console.log
}
});
wasmInstance = results.instance.exports;
});
...

Jest provides life-cycle methods to perform any setup or teardown actions prior to running tests. You can specify functions to run before or after all of the tests (beforeAll()/afterAll()), or before or after each test (beforeEach()/afterEach()). We need a compiled instance of the Wasm module from which we can call exported functions, so we put the instantiation code in the beforeAll() function.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $19.99/month. Cancel anytime

We're wrapping the entire test suite in a describe() block for the file. Jest uses a describe() function to encapsulate suites of related tests and test() or it() to represent a single test. Here's a simple example of this concept:


const add = (a, b) => a + b;


describe('the add function', () => {
test('returns 6 when 4 and 2 are passed in', () => {
const result = add(4, 2);
expect(result).toEqual(6);
});

test('returns 20 when 12 and 8 are passed in', () => {
const result = add(12, 8);
expect(result).toEqual(20);
});
});

The next section of code contains all the test suites and tests for each exported function:


...
  describe('the _addTwoNumbers function', () => {
    test('returns 300 when 100 and 200 are passed in', () => {
      const result = wasmInstance._addTwoNumbers(100, 200);
      expect(result).toEqual(300);
    });


test('returns -20 when -10 and -10 are passed in', () => {
const result = wasmInstance._addTwoNumbers(-10, -10);
expect(result).toEqual(-20);
});
});

describe('the _divideTwoNumbers function', () => {
test.each([
[10, 100, 10],
[-2, -10, 5],
])('returns %f when %f and %f are passed in', (expected, a, b) => {
const result = wasmInstance._divideTwoNumbers(a, b);
expect(result).toEqual(expected);
});

test('returns ~3.77 when 20.75 and 5.5 are passed in', () => {
const result = wasmInstance._divideTwoNumbers(20.75, 5.5);
expect(result).toBeCloseTo(3.77, 2);
});
});

describe('the _findFactorial function', () => {
test.each([
[120, 5],
[362880, 9.2],
])('returns %p when %p is passed in', (expected, input) => {
const result = wasmInstance._findFactorial(input);
expect(result).toEqual(expected);
});
});
});

The first describe() block, for the _addTwoNumbers() function, has two test() instances to ensure that the function returns the sum of the two numbers passed in as arguments. The next two describe() blocks, for the _divideTwoNumbers() and _findFactorial() functions, use Jest's .each feature, which allows you to run the same test with different data. The expect() function allows you to make assertions on the value passed in as an argument. The .toBeCloseTo() assertion in the last _divideTwoNumbers() test checks whether the result is within two decimal places of 3.77. The rest use the .toEqual() assertion to check for equality.

Writing tests with Jest is relatively simple, and running them is even easier! Let's try running our tests and reviewing some of the CLI flags that Jest provides.


Running the wasm tests

To run the tests, open a terminal instance in the /chapter-09-node/testing-example folder and run the following command:


npm test

You should see the following output in your terminal:


main.wasm Tests
  the _addTwoNumbers function
    ✓ returns 300 when 100 and 200 are passed in (4ms)
    ✓ returns -20 when -10 and -10 are passed in
  the _divideTwoNumbers function
    ✓ returns 10 when 100 and 10 are passed in
    ✓ returns -2 when -10 and 5 are passed in (1ms)
    ✓ returns ~3.77 when 20.75 and 5.5 are passed in
  the _findFactorial function
    ✓ returns 120 when 5 is passed in (1ms)
    ✓ returns 362880 when 9.2 is passed in


Test Suites: 1 passed, 1 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 1.008s
Ran all test suites.

If you have a large number of tests, you could remove the --verbose flag from the test script in package.json and only pass the flag to the npm test command if needed. There are several other CLI flags you can pass to the jest command. The following list contains some of the more commonly used flags:


  • --bail: Exits the test suite immediately upon the first failing test suite
  • --coverage: Collects test coverage and displays it in the terminal after the tests have run
  • --watch: Watches files for changes and reruns tests related to changed files

You can pass these flags to the npm test command by adding them after a --. For example, if you wanted to use the --bail flag, you'd run this command:


npm test -- --bail

You can view the entire list of CLI options on the official site at https://jestjs.io/docs/en/cli.

In this article, we saw how the Jest testing framework can be leveraged to test a compiled module in WebAssembly to ensure it's functioning correctly. To learn more about WebAssembly and its functionalities read the book, Learn WebAssembly.




Blazor 0.6 release and what it means for WebAssembly

Introducing Wasmjit: A kernel mode WebAssembly runtime for Linux.

Why is everyone going crazy over WebAssembly?