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!