Jest + jQuery for testing a vanilla “app”

May 15th, 2014. Tagged: JavaScript, tools

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);

The full source is here

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!

Tell your friends about this post: Facebook, Twitter, Google+

Sorry, comments disabled and hidden due to excessive spam. Working on restoring the existing comments...

Meanwhile, hit me up on twitter @stoyanstefanov