Unit-Test JavaScript of Plain Webpages using Jest

Sep 5, 2020

I like to hack little tools together using HTML and plain, old, simple JavaScript. The setup is ridiculously simple, iterations are fast and it runs virtually everywhere. I want to keep those tools as plain and simple as possible without any frameworks like Angular or React. But still, I don’t want to miss automated tests.

In this post, I want to show a simple setup for automated tests for JavaScript on a plain static webpage using Jest. Jest is a JavaScript Testing Framework focusing on simplicity. Exactly what I want.

Example Project

The file structure of the example project looks like this:

── example
   ├─ public
   │  ├─ index.html
   │  └─ greeter.js
   └─ test
      └─ greeter.test.js

Everything in public is deployed to a web-server. test contains automated tests that are not deployed. The content of those example files looks like this:

greeter.js:

const getGreetingText = (hourOfDay) => {
    if (hourOfDay < 12) {
        return "Good morning!"
    } else if (hourOfDay < 17) {
        return "Good afternoon!"
    } else {
        return "Good evening!"
    }
};

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Example</title>
</head>
<body>
  <span id="greeting"></span>
  <script src="greeter.js"></script>
  <script>
    document.getElementById('greeting').innerText = getGreetingText(new Date().getHours());
  </script>
</body>
</html>

greeter.test.js:

const [getGreetingText] = require('../public/greeter');

test('greets with "Good morning!" at 11:00', () => {
  expect(getGreetingText(11)).toBe('Good morning!');
});

test('greets with "Good afternoon!" at 12:00', () => {
    expect(getGreetingText(12)).toBe('Good afternoon!');
});

test('greets with "Good evening!" at 17:00', () => {
    expect(getGreetingText(17)).toBe('Good evening!');
});

Setup Jest

So far so good. index.html works. But we also want to verify getGreetingText using the tests in greeter.test.js. To run the tests we need to install the Jest CLI.

I’ll show you how to install Jest into a Docker container. That is ideal to try it out before installing it directly to your system because it’s very easy to just remove the container again if it turns out not being useful. Alternatively, you can follow the instructions on the official Jest Getting Started page for the CLI.

1 Create container

Let’s create a Docker container based on alpine with a shared folder with the host system:

mkdir shared
docker run -v $PWD/shared:/home/shared -it alpine:3.12 /bin/ash

If you don’t know what all of this means, check out the Installing Hugo into a Docker Container post for more details.

2 Install Jest via Yarn

You are now inside the Docker container. To install Jest we first need a package manager that can install it. We use yarn as suggested in the official guide:

apk add yarn
yarn --version # verify installation was successful
yarn global add jest
jest --version # verify installation was successful

3 Configure Jest

Before we run the tests, make sure that the example project presented at the top lies in the shared/example directory, so it’s also accessible within the Docker container. In the Docker container we now try to run the tests:

cd /home/shared/example
jest

But we’ll receive this error:

Error: Could not find a config file based on provided values:
path: "/home/shared/example"
cwd: "/home/shared/example"
Config paths must be specified by either a direct path to a config
file, or a path to a directory. If directory is given, Jest will try to
traverse directory tree up, until it finds one of those files in exact order: "jest.config.js" or "jest.config.mjs" or "jest.config.cjs" or "jest.config.json".

To get Jest working we need a configuration file. So let’s add a jest.config.js file to the project root as suggested by the official documentation:

── example
   ├─ jest.config.js
   ...

jest.config.js:

module.exports = {
    verbose: true,
};

Let’s try again to run jest. We’re one step further now but we still receive an error:

 FAIL  test/greeter.test.js
  ● Test suite failed to run

    TypeError: require is not a function or its return value is not iterable

    > 1 | const [getGreetingText] = require('../public/greeter');
        |                           ^
      2 | 
      3 | test('greets with "Good morning!" at 11:00', () => {
      4 |   expect(getGreetingText(11)).toBe('Good morning!');

      at Object.<anonymous> (test/greeter.test.js:1:27)

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        1.429 s
Ran all test suites.

The problem is that the test cannot access the getGreetingText function. We’ll fix this in the next step.

4 Export Function under Test

Jest was designed for way bigger projects, with multiple files, internal and exposed functions, and much more. To comply with this setup we need to “export” our getGreetingText function from the greeter.js file and make it available for greeter.test.js. Add this line to greeter.js export the function:

module.exports = [getGreetingText];

5 Run the Tests

Finally, we’re able to run the tests! Make sure you are in the project root (example folder) and run jest. The output looks like this:

 PASS  test/greeter.test.js
  ✓ greets with "Good morning!" at 11:00 (3 ms)
  ✓ greets with "Good afternoon!" at 12:00
  ✓ greets with "Good evening!" at 17:00 (1 ms)

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

6 Bonus: Fix Browser Error

We did it! The tests run through with Jest and it also works in the Browser! The only thing that is still a bit unpleasant is that the Browser’s console shows an error:

Uncaught ReferenceError: module is not defined
    <anonymous> file:///.../shared/example/public/greeter.js:11

This is because the browser doesn’t know about the module.exports notation.

To workaround that error we can just define a module object if it doesn’t exist yet. This avoids the error in the browser and doesn’t interfere with Jest. It isn’t the most beautiful solution but it’s simple and does the trick. Just add this line in greeter.js just above the module.exports declaration and the error should be gone:

if (typeof module == 'undefined') { var module = {}; }

Conclusion

We’re done! We have a simple static webpage where we can write tests for the JavaScript part. The final file structure looks like this:

── example
   ├─ jest.config.js
   ├─ public
   │  ├─ index.html
   │  └─ greeter.js
   └─ test
      └─ greeter.test.js

And the only change that was necessary in the code to get it running was adding in greeter.js:


if (typeof module == 'undefined') { var module = {}; }
module.exports = [getGreetingText];

By running jest within the example folder our tests are executed.

While there was a little effort needed to get the setup done, we only needed a little configuration to get it all running. Still, if you know a simpler and more minimalistic solution, I’m happy to hear it!

You might also like