Convention based tests using Jasmine preventing common errors in HTML templates
Problem statement
We keep getting bugs introduced due to typos or small errors in the way we bind our views to our view models. There is also a set of problems where given the choice we occasionally choose, by mistake, a different kind of binding causing slight differences in behavior between views.
Our setup
Our client side code focuses on being easy to test in the parts that we change the most. In our application, that would be the views, or pages.
Our application is built using Knockout.js and Sammy.js setup with a Browserify as a modular loader. It's tested using Jasmine.
This setup allows us to create views and test their view models effectively.
Our tests need just a single line to require the service under test, and the templates the view models bind to is directly available as a module.
var template = require('templates/<template_name>');
var viewmodel = require('viewmodels/<viewmodel_name>');
To then make sure that a view follows a basic physical layout of files we use Scaffolt to generate the structure needed for the routing conventions setup in Sammy.js
Thus when we want a new view, Scaffolt sets up three files for us
app/templates/<viewname>
app/viewmodels/<viewname>
spec/viewmodels/<viewname>
The template contains the HTML, the view model the behavior and the spec file contains the tests ensuring the view model works as intended.
The scaffolded spec file
Along with the other files, Scaffolt sets up the spec file for us. Just as the view model follows a set of conventions so does the spec file for the view model.
We know that the view model contains a function called load, thus the spec file has a method to test said function.
The spec file is the hero of this post so lets see what it looks like when generated.
describe('the test view',function(){
'use strict';
var vm = require('viewmodels/test');
it('should be defined', function(){
expect(vm).toBeDefined();
});
ensureViewModelCanBindToTemplate('test');
describe('when loading',function(){
beforeEach(function() {
vm.load();
});
it('should do this',function(){
});
});
});
There are some simple tests here. It checks that it can find the view model with the given name.
There is a simple empty test for the load function, ready to be filled with logic.
There is also this line:
ensureViewModelCanBindToTemplate('test');
This is a function call with the view name as an argument. Remember that since we follow a strict convention, that name is all we need to get both the template (html) for the view and its view model.
Convention based tests
A neat thing with JavaScript, and similar languages such as Ruby, is that a function can return a new function. The new function can use the arguments as constants inside its execution.
Combined with Jasmine (this works with RSpec too) we can use a function to create tests.
This is what ensureViewModelCanBindToTemplate
does. It's defined in our spec_helper.js
file and was originally used to ensure that the viewmodel and its template could work together.
This is what it looked like
window.ensureViewModelCanBindToTemplate = function( templateName ) {
it('should be able to bind to template', function() {
var vm = require('viewmodels/'+ templateName);
var div = document.createElement('div');
jQuery(div).html(require('templates/' + templateName));
ko.applyBindings( vm, div );
ko.cleanNode( div );
});
};
The function adds a test that will check that there is a template for the view model, and that Knockout.js can bind the two together.
Last week I added more responsibility to this poor function. I started checking the HTML in the template, ensure we follow a set of conventions.
Here is an example of what I added:
window.ensureViewModelCanBindToTemplate = function( templateName ) {
it('should be able to bind to template', function() {
...
});
it('will have ids on buttons', function () {
var vm = require('templates/' + templateName);
var buttons = $('button', $(vm));
buttons.each(function (index, button) {
if(button.type !== 'submit' && button.type !== 'reset') {
expect($(button).attr('data-bind')).toBeTruthy('no binding on button: ' + button.outerHTML);
}
expect(button.id).not.toEqual('', 'button missing id: ' + button.outerHTML);
if(button.id) {
expect(button.id.indexOf('btn_')).toEqual(0, 'button id: '+button.id+' does not start with "btn_"');
}
});
});
};
This new generated test uses JQuery to check all buttons inside the template to ensure they have an ID attribute set.
It checks that the ID follows our naming convention. It also checks that there is a Knockout binding on the button unless it's a button used for submitting or reseting a form.
This kind of testing on the HTML of the template allows us to check the following:
- We use the right type of Knockout.js binding for our form elements
- We use a form submit binding to ensure user expectations on forms
- We check all buttons and input fields for ID's and conventions
- We ensure unique id's inside the template, preventing ugly copy'n'paste errors
These types of tests also ensure we create better Selenium based tests for our application, as we now know that all important elements have ID's and what naming convention they follow.
We can compare this type of testing to what linting or Code Analysis warnings gives us, except now its for our HTML templates using our own conventions.
Conclusion
Using convention and scaffolt can allow us to setup a series of tests that all views or templates have in common.
- Ensuring that our users know what behavior to expect
- Helps us prevent simple typing error mistakes in the bindings
- Make it easier for us to write selenium type tests for our applications