Jest is a new javascript testing tool announced today. I thought I'd take it out for a spin testing the UI of a simple vanilla JS app - no CommonJS modules, no fancy stuff. Just old school JavaScript. Granted, it's probably not what the tool was optimized to do, but it totally worked.
The app
It's a simple HTML page with inline CSS and JS that validates username and password and paints some of the UI in red if there's a validation error. Give it a try
Markup:
<p class="error error-box hidden" id="err">Please fill in the required fields</p> <form onsubmit="return validateSubmit(this)" method="post" action="/cgi-bin/perlbaby.pl"> <ul> <li><label id="username-label" for="username">Username</label> <input id="username"></li> <li><label id="password-label" for="password">Password</label> <input id="password"></li> </ul> <button type="submit" id="button">go</button> </form>
CSS:
.hidden {display: none} .error {color: red} .error-box {border: 1px solid red}
When the user submits the form, the function validateSubmit()
is called to do the validation. There's no framework so everything is pretty old school:
function validateSubmit(f) { var validates = true; ['username', 'password'].forEach(function(field) { if (!document.getElementById(field).value) { validates = false; document.getElementById(field + '-label').className = 'error'; } else { document.getElementById(field + '-label').className = ''; } }); document.getElementById('err').className = validates ? 'hidden' : 'error error-box'; if (validates) { // fancy stuff goeth here } return false; }
Actually it was even older school, but the test didn't quite work because JSDOM which is used behind the scenes for the DOM stuff doesn't support ancient stuff like accessing form elements of the sort: document.forms.username
. JSDOM also don't seem to support classList
property at the moment, which is a bummer, but I'm sure will be added eventually. Anyway.
Feel free to play with the page and try to submit emtpy fields to see the UI changes
OK, so how do you test that this page behaves as expected. Enter Jest.
Jest
To install Jest, go
$ npm install -g jest-cli
You then need to create a package.json
file where your app lives, like:
{ "scripts": { "test": "jest" } }
Now you're ready to run tests!
$ cd ~/apps/app $ mkdir __tests__ $ npm test > @ test ~/apps/app/jester > jest Found 0 matching tests... 0/0 tests failed Run time: 0.596s
Cool, it works! Only there are no tests to run.
A test example
If you're familiar with Jasmine for JS testing... well, Jest extends that so the syntax is the same. Here's a barebone minimal example:
describe('someName', function() { it('does stuff', function() { // ... expect(true).toBeTruthy(); }); });
Put this in your app's __tests__
directory so Jest knows where to find and run:
$ npm test
> @ test ~/apps/app/jester
> jest
Found 1 matching tests...
PASS __tests__/example.js (0.016s)
0/1 tests failed
Run time: 1.305s
Or how about making the test fail, just for kicks:
describe('someName', function() { it('does stuff', function() { // ... expect(true).toBe(1); }); });
Running...
$ npm test > @ test ~/apps/app/jester > jest Found 1 matching tests... FAIL __tests__/example.js (0.017s) ◠someName › it does stuff - Expected: true toBe: 1 at Spec.(~/apps/app/jester/__tests__/example.js:4:18) at Timer.listOnTimeout [as ontimeout] (timers.js:110:15) 1/1 tests failed Run time: 1.405s
Not bad. Now let's do a real example.
Testing the vanilla
The thing about Jest is that it mocks everything. Which is priceless for unit testing. But it also means you need to declare when you don't want something mocked. Starting the new test with:
jest .dontMock('fs') .dontMock('jquery');
"Huh?!" you say. jQuery? Yup, I used jQuery to do the DOM-y stuff in the test. Like submit the form and check for class names, fill out the form, and... no, that's about it. You, of course, can use any library that JSDOM can handle.
The magic of Jest is in its use of require()
for all the mocking. Read more here. So any module you require will be mercilessly mocked unless you say dontMock()
.
Moving on.
I'll fetch the markup (that includes the inline JavaScript) so I can test it later. Oh, and require jQuery:
var $ = require('jquery'); var html = require('fs').readFileSync('./app.html').toString();
Now, you know the "template" for a new test. Let's have two of these:
describe('validateSubmits', function() { it('shows/hides error banner', function() { // ... test here }); it('adds/removes error classes to labels', function() { // ... test here }); });
test #1
First set the content of the empty document that the framework has created with the contents of the app read from disk:
document.documentElement.innerHTML = html;
Next, checking the initial state. In the initial state the error message is hidden with a CSS class name .hidden
since there are no errors. So here comes the jQuery magic combined with Jasmine's:
// initial state expect($('#err').hasClass('hidden')).toBeTruthy();
Next, submit the form without filling it out. Error state ensues. The error message paragraph is now displayed because our app removed the .hidden
class:
// submit blank form, get an error $('form').submit(); expect($('#err').hasClass('hidden')).toBeFalsy();
Finally, test that the error message is again hidden after the form is filled out and submitted:
// fill out completely, error gone $('#username').val('Bob'); $('#password').val('123456'); $('form').submit(); expect($('#err').hasClass('hidden')).toBeTruthy();
Test #2
The second test is similar, only this time we're checking if the form labels have .error
class which makes 'em all red. Here goes:
document.documentElement.innerHTML = html; // initially - no errors expect($('#username-label').hasClass('error')).toBe(false); expect($('#password-label').hasClass('error')).toBe(false); // errors $('form').submit(); expect($('#username-label').hasClass('error')).toBe(true); expect($('#password-label').hasClass('error')).toBe(true); // fill out username, missing password still causes an error $('#username').val('Bob'); $('form').submit(); expect($('#username-label').hasClass('error')).toBe(false); expect($('#password-label').hasClass('error')).toBe(true); // add the password already $('#password').val('123456'); $('form').submit(); expect($('#username-label').hasClass('error')).toBe(false); expect($('#password-label').hasClass('error')).toBe(false);
Thanks!
Thanks for reading! Now, I'm sorry to inform you, you have no excuse not to write tests. Even this old school page can be tested, imagine what you can do with your awesome fancy JS modules!
Comments? Find me on BlueSky, Mastodon, LinkedIn, Threads, Twitter