Back to home

Jest Snapshot Testing For Template Languages main image

Jest Snapshot Testing For Template Languages

This post will cover a great use case for Jest Snapshot testing: template engines and generators.

We can use Jest to assert that our expectations for a generated template match the actual output, particularly as adjustments are made over time.

Source code can be found here

Prerequisites

  1. Basic familiarity with npm.
  2. Basic familiarity with Node.js.

What is snapshot testing?

The official Jest docs describe snapshot testing as "...a very useful tool whenever you want to make sure your UI does not change unexpectedly."

During a snapshot test, Jest will take a snapshot and then compare it to a reference snapshot file alongside the test.

Typically, this is used for the UI, but it doesn't take much of an online search to note that they are a hotly debated topic.

Snapshots for the UI in the wild can cause false-negatives to occur when minor changes are made to a rendered component that don't have an impact on the final rendered output.

For example, take the following before and after example of a component:

// Before const Component = () => { return <div>Hello World</div>; }; // After const Component = () => { return <div data-testid="test-div">Hello World</div>; };

Making a simple change like this can result in a false-negative that may slow down the development process or even impact CI/CD build pipelines. The fact that the UI is often iterated on also makes it more frequent than you make think to come across this issue.

That being said, I believe there is a strong use-case for snapshot testing, but as opposed to it being for the UI it is in fact for template languages!

What is a template language?

Picking off an answer from StackOverflow to help us find a working definition:

"The premise behind a template language is that the language is "embedded" within some other master document."

That is to say, we can use a template language to help us generate code (or other output).

An example of this using the template language ejs would be the like so.

First, let's define an EJS template file index.ejs using valid EJS:

<h2>Hello, <%= name %>!</h2>

The <% = name %> will render a name variable passed as data at the time we render the template.

If we run the EJS compiler on the template file the argument World, we will get the following output:

<h2>Hello, World!</h2>

Using template languages can be a great way to skip over mundane tasks, but as you can see it can be susceptible to change.

We can use the power of snapshot testing. As mentioned above, the two pain-points for snapshot testing are:

  1. False-negatives that don't impact the desired outcome.
  2. UI code goes through a lot of iteration.

With template languages, the desired outcome always changes when the template file is changed, and once we have a desired output from a template language, it is not often that we wish to change the values.

Note: Purely based on my experience, it is not often you change the values of a template file after reaching a desired outcome.

In the following example, we will use the ejs template language to generate a simple HTML page and demonstrate the use of Jest snapshot testing with it.

Getting started

We will first initialize a new NPM project in the directory jest-snapshot-testing-for-template-engines. Afterwards, we will create some files and folders required for the demonstration.

$ mkdir jest-snapshot-testing-for-template-engines $ cd jest-snapshot-testing-for-template-engines # Initialise npm project with basics $ npm init -y $ npm install ejs $ npm install -D jest # Create files and folders for the demo $ mkdir __tests__ templates $ touch index.js ejs.js __tests__/ejs.test.js templates/hello.ejs

At this stage, our project is now ready to start working with.

Adding our template file

Edit templates/hello.ejs to have the following:

<h2>Hello, <%= name %>!</h2>

Writing our helper function for the template file

We are going to programmatically generate a template file using the template language.

In ejs.js, add the following:

const ejs = require("ejs"); const fs = require("fs"); const path = require("path"); const filepath = path.resolve(__dirname, "./templates/hello.ejs"); const helloTemplate = async (data) => { const template = fs.readFileSync(filepath, "utf8"); const str = await ejs.render(template, data, { async: true, }); return str; }; module.exports = { helloTemplate, };

Our helloTemplate function can take an argument data which is an object containing a name property.

What we pass as the value for name will be used to help output a value.

Writing our script to test the template file

In index.js, add the following:

const fs = require("fs"); const { helloTemplate } = require("./ejs"); const output = "./output.html"; async function main() { const str = await helloTemplate({ name: "World" }); fs.writeFileSync(output, str); } main();

If we now run node index.js in our terminal, we will have a new output file output.html with the following content:

<h2>Hello, World!</h2>

If you alter what is passed as the argument in index.js to our helloTemplate function, you will see the corresponding output.

Aces! So now that we have our desired output, how do we test it?

Snapshot testing with Jest

In our __tests__/ejs.test.js file, add the following:

const { helloTemplate } = require("../ejs"); describe("hello template file", () => { test("expect helloTemplate fn output to match snapshot", () => { const data = { name: "World" }; const str = helloTemplate(data); expect(str).toMatchSnapshot(); }); });

We are using the toMatchSnapshot matcher to test our output.

In order to run the test, update the package.json test script to be "test": "jest" and run npm run test to see what happens.

$ npm run test > jest-snapshot-testing-for-template-engines@1.0.0 test > jest PASS __tests__/ejs.test.js hello template file ✓ expect helloTemplate fn output to match snapshot (6 ms) › 1 snapshot written. Snapshot Summary › 1 snapshot written from 1 test suite. Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 1 written, 1 total Time: 0.735 s, estimated 1 s Ran all test suites.

On our first run, we can see that a snapshot was written. If we now check our __tests__ folder, you will see a __snapshots__ folder with a file ejs.test.js.snap with the followinging:

// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`hello template file expect helloTemplate fn output to match snapshot 1`] = ` "<h2>Hello, World!</h2> " `;

Within the export, we can see our <h2>Hello, World!</h2> output!

Note: In our basic demonstration, we are not using libraries such as Prettier to format our code.

Let's now update our hello.ejs template to something different:

<h2>Hello, <%= name %>!</h2> <p>This is something new that we want as part of the output</p>

Our second line now contains another static <p> tag that we have decided we want to add into our script.

If we run the test again, we can see that we get a failure because the output no longer matches.

$ npm run test > jest-snapshot-testing-for-template-engines@1.0.0 test > jest FAIL __tests__/ejs.test.js hello template file ✕ expect helloTemplate fn output to match snapshot (8 ms) ● hello template file › expect helloTemplate fn output to match snapshot expect(received).toMatchSnapshot() Snapshot name: `hello template file expect helloTemplate fn output to match snapshot 1` - Snapshot - 1 + Received + 2 - <h2>Hello, World!</h2> + <h2>Hello, World!</h2> + <p>This is something new that we want</p> ↵ 6 | const str = await helloTemplate(data); 7 | > 8 | expect(str).toMatchSnapshot(); | ^ 9 | }); 10 | }); 11 | at Object.<anonymous> (__tests__/ejs.test.js:8:17) › 1 snapshot failed. Snapshot Summary › 1 snapshot failed from 1 test suite. Inspect your code changes or run `npm test -- -u` to update them. Test Suites: 1 failed, 1 total Tests: 1 failed, 1 total Snapshots: 1 failed, 1 total Time: 0.564 s, estimated 1 s Ran all test suites.

Given that this is something that we have confirmed as a developer that we want, we can update the snapshot now with npm run test -- -u.

npm run test -- -u> jest-snapshot-testing-for-template-engines@1.0.0 test > jest "-u" PASS __tests__/ejs.test.js hello template file ✓ expect helloTemplate fn output to match snapshot (6 ms) › 1 snapshot updated. Snapshot Summary › 1 snapshot updated from 1 test suite. Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 1 updated, 1 total Time: 1.896 s Ran all test suites.

Thanks to this, we have a more meaningful way to ensure that our output matches our expectations. Given that changes to template languages always change the outcome, snapshots are well worth the use.

It also now means that our snapshot that is updated will be part of an PR to ensure that the reviewer can confirm that this is the desired output.

Finally, the last benefit is that we do not have to write tests that generate the output into a file and read/assert the file to our expectation.

Summary

Today's post demonstrated a great use case for snapshot testing when it comes to generating files with template languages.

We did so by talking about both snapshot testing and language templates, followed up by a demonstration of one in action.

Resources and further reading

Photo credit: martinadams

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on workingoutloud.dev, Den Dribbles and LandPad .

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.