Testing your Javascript UI with simulated events
When it comes to testing Javascript UI, two main options come to mind:
- doing it by hand
- running something like Selenium or CasperJS/PhantomJS
But a third option is available: creating and triggering events with Javascript code to "unit test" your UI.
Why would I do that?
Of the 3 options above, the first one is the closest to what will actually happen when people use your app. A real person, with a real browser goes through scenarios and verifies what's displayed on the screen. Sounds slow and boring... and it is. But sometimes, it's the only way to test specific features in your app.
The second option brings in automation. No more boredom for a human, computer will run the tests on its own, faster than a human does. Tests are still bound to the latency of the HTTP requests your app makes, though, which can make them take some time to run, still. It is also tricky to isolate parts of the apps. But this option is great to automate end-to-end testing of your app.
Simulating events in Javascript adds in isolation. It helps with the setup. You can run your test against a specific set of HTML, with a specific set of data... you don't need to navigate here and there to get to a specific screen.
It also gives more control over the validation. You can check the text contain in elements, the classes and attributes they have just like with Selenium/CasperJS/PhantomJS. But you also get access to deeper levels of your application. You can check that variables in your code actually get changed when the user interacts. This can also save you some navigation by just checking that your widget sends the appropriate HTTP request to your server upon interaction.
Sounds cool, show me!
Just as with any other tests, there'll be two things you'll need to do when testing with simulated events:
- executing some actions
- validating their results
Simulated events help with the former. So let's say we want to test this simple click counter: you click in a trigger, it increments the value in a display.
var clickCounter = {
setup: function (display, trigger) {
this.value = 0;
this.display = display;
this.trigger = trigger;
this.trigger.addEventListener('click', this.incrementCounter.bind(this));
this.refreshDisplay(this.value);
},
incrementCounter: function () {
this.value += 1;
this.refreshDisplay(this.value);
},
refreshDisplay: function (value) {
this.display.textContent = value;
if (value) {
this.display.classList.add('clickCounter-notNull');
}
}
};
The test needs a little bit of setup: one element to be the trigger, one element to be the display, and the clickCounter
setup to use them. I ran into a few weird things when the elements are not actually in the page, especially when testing things relying on event bubbling. So I find it best to add the elements to the DOM during the setup and remove them during the teardown.
describe('clickCounter', function () {
beforeEach(function () {
this.trigger = document.createElement('button');
this.display = document.createElement('div');
clickCounter.setup(this.trigger, this.display);
document.body.appendChild(this.trigger);
document.body.appendChild(this.display);
});
afterEach(function () {
document.body.removeChild(this.trigger);
document.body.removeChild(this.display);
});
// To be continued...
}
First thing to do is to locate the element you want to trigger your event from. querySelector
, jQuery's $
or maybe the el
property of a Backbone view, the options are plenty and the same you would use when you need to select an HTML element in your app. As the setup created specific elements for the test and stored them in the test context, no need to look them up :)
And then comes the time to create the event and trigger it. IE (10 at least) has trouble with the latest new Event()
syntax, so the test will use good ol' document.createEvent()
method.
Regarding the validations, there are 3 things we can validate:
- that the clickCounter.value
variable is actually incremented
- that the display actually shows the new value
- that the display gets a specific class when it displays a non null value.
This give us the following test:
// ... Continuing from where we left
it('Should increment the value in the display', function () {
var event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true);
this.trigger.dispatchEvent(event);
expect(clickCounter.value).to.equal(1);
expect(this.display.textContent).to.equal('1');
expect(this.display.classList.contains('clickCounter-notNull').to.be.true;
}
Awesome, I'll just use those then!
Not quite. Each of the 3 options presented at the start has its place for testing your Javascript UI. Simulated events are great to "unit test" your UI functions and components, providing a first level of testing that will be fast to run. Selenium/CasperJS/PhantomJS will allow you to run wider end-to-end testing against a full deployment of your app. And for times when Selenium/CasperJS/PhantomJS doesn't cut it, good ol' human testing comes to the rescue. So go ahead and start testing your UI with simulated events, but don't forget the other methods ;)