Navigate / search

Test-Driven HTML: Why and How

I’ve been racking my brain for a week straight on the best way to employ test-driven principles to HTML. Hell, I’ve been trying to figure out if it even can be done. Trying to figure out if it’s even worth it. Mostly through blind faith, I’ve come up with the beginning of what I hope becomes a winning strategy for test-driven HTML development.

“…the beginning of what I hope becomes a winning strategy…” – lol, how’s that for confidence :)

Test-Driven HTML5

What Exactly Are We Testing?

The look and feel of the UI is handled through CSS. If you’re still using tables for layout then there may be more value here for you, but if that’s the case, seriously, stop reading this right now and catch up on the last 15 years of web design. We’ll be waiting. Ok, back to 2012. If the “look and feel” are handled by CSS and the fancy stuff is handled by Javascript (e.g. AJAX, client-side form validation, image sliders, etc), then what’s left to test?

  1. Do your links go where they’re suposed to? – because you’re dedicated to unobtrusive Javascript, all your links have an HREF attribute, right?
  2. Do your view templates render their content into the right DIV?
  3. Do your forms redirect appropriately when submitted? – or not redirect as the case may be
  4. Do portions of your home page fail to load when you change the JSON output of a seemingly unrelated API endpoint (that happened to me)
  5. Most importantly, can you refactor your views and layouts with confidence?

If you can already answer “yes” to the questions above, then skip testing your HTML. It’s not necessary. I don’t say that rhetorically either; it’s possible to manually test your site in such a way that you can answer “yes” to everything on that list. My guess is you are the minority, however. Personally, I hate debugging. It’s time consuming, and it’s usually a bug caused by a missing semicolon or something stupid. I want to spend my time solving problems, not manually linting code. Hence my obsession with testing. I’ve saved countless hours over the past few months due to well thought out, well written, and automated tests. Now you can too.

Linting

Linting is a given, or at least it should be. Make sure your code is clean. Again, this can be done manually by copy/pasting your HTML into one of the dozens of HTML linting websites and clicking “Go”. For the ambitiously lazy people like myself, we automate. I use Perl’s HTML::Lint (easily installed using CPAN) in a simple script that I run with a Jakefile. In my implementation there are a lot of config options and such that I’m leaving out for brevity; follow the GitHub repo and I’ll eventually update it with my full code.

Perl Script:

#!/usr/bin/env perl

use strict;
use HTML::Lint;

if (@ARGV == 0) { die("You must provide a file to lint.\n"); }

my $linter = HTML::Lint->new;

my $numerrors = 0;

foreach my $arg (@ARGV) {
  $linter->parse_file($arg) or die("Cannot lint file: ", $arg, "\n");

  if ($linter->errors > 0) {
    $numerrors += scalar($linter->errors);

    foreach my $error ($linter->errors) {
      print $arg, ": ", $error->errtext(), " at ", $error->where(), "\n";
    }
  } else {
    print $arg, " ok.\n";
  }
}

if($numerrors > 0) {
  print "-----------------\n";
  print "Total Errors: ", $numerrors, "\n";
  exit 1;
} else {
  print "-----------------\n";
  print "No errors found.\n";
  exit 0;
}

Jakefile:


(function() {
  'use strict';

  task('default', ['lint']);

  desc('Lint our layouts, views, and templates');
  task('lint', function() {
    //get the correct files for linting
    var files = new jake.FileList();
    //including all 3 lines below may be overkill,
    // but it helps illustrate some options
    files.include('index.html');
    files.include('src/*.html');
    files.include('src/**/*.html');
    //don`t lint our tests
    files.exclude('test');

    var fileNames = files.toArray().join(' ');
    var perlLintCmds = [ 'perl ./html_lint.pl ' + fileNames];

    var exec = jake.createExec(perlLintCmds, { stdout: true });

    exec.addListener('cmdEnd', function() {
      //since we only execute 1 command we don't need to count the number
      // of commands to determine when we're done. just finish.
      complete();
    });

    exec.addListener('error', function(msg, code) {
      //just add a blank space to make the output more readable
      console.log('');
      //fail the Jake test
      fail(msg, code);
      complete();
    });

    exec.run();

  }, {async: true});
}());

Run the Linter:

$ jake lint

Semantics And Document Structure

I haven’t seen anything to test for semantically correct HTML as this would require someone to interpret the meaning of the content of your tags (highly improbable and subject to interpretation). There may be opportunity in building a markup language that expresses the intent of your content and creates the appropriate HTML tag for its components.  I envision something similar to jade, except instead of tag names preceding a line, it would be an intention descriptor.  Fun to think about, but not something I’m passionate enough about to dedicate any significant time to.  If you decide to write one though, let me know.  I’d love to contribute.

Basic HTML5 Document Structure
Basic HTML5 Document Structure

Testing the document structure seems kind of pointless with so many people using jade, ejs, haml, etc. I was working on a YAML interpretation of document structure that, combined with jQuery, would load a page and test the structure was correct. the YAML ended up looking a lot like jade though so I dropped it.  It felt like I was trying to employ test-driven principles to writing copy…that’s going a little overboard IMHO.

CSS tests catch most anything relating to the UI anyways (more on testing CSS to come). If your CSS tests are solid then anything the user actually cares about will be tested by a combination of CSS and functional testing.  Which brings use to our next topic…

Functional Testing

Now that we can lint our code with easy, we can start on functional testing.  Functional testing is where I see the most value in testing HTML.  So, how do we write tests first to keep our code lean and mean?  I recommend a 2-step approach.  Headless browser tests first for the sake of speed, then in-browser tests for compatibility.  If you don’t know what a headless browser is or the pros and cons of using one, please Google it real quick. I’m a huge fan of almost everything Learnboost and TJ Holowaychuk have put out for the community, so I used Tobi for a long time and I loved it, but it’s so out of date now that it’s no longer useful :(.

For this project, I looked at Zombie, but they really want you to run your app and test the whole stack.  Since this is unit testing, we’re looking to test the smallest portion of HTML that we can.  That’s definitely not the full stack.  After some research, I’ve decided to give Buster.JS a shot despite being in beta still.  It’s actively being developed by Christian Johansen who literally wrote the book on Javascript TDD so I’m pretty confident it’ll work for our simple HTML tests.

Enough talk.  How do we get rolling with test-driven HTML?  First install NodeJS, then create a directory and install BusterJS via NPM and download the PhantomJS binary for your system and extract it to ./lib/phantomjs. Automatic headless testing is coming to Buster, but it’s not quite there yet.  In the meantime, they’ve provided us with a handy script to configure PhantomJS for use with Buster.

Execute the following to start the Buster test server and an instance of PhantomJS with the correct settings.


buster server &
./lib/phantomjs/bin/phantomjs ./node_modules/buster/script/phantom.js &

Great, now for the Buster config file.  (I swear this is the last step before actually writing some tests).  Also, I downloaded jQuery and placed it in the ./lib folder because it makes DOM stuff really easy so we can concentrate on writing solid tests.  If you’re not using jQuery in your project, use whatever library your project is built around to ensure compatibility.

buster.js


var fs = require('fs');

var config = module.exports;

config["TDD HTML Headless"] = {
  environment: "browser",
  libs: [
    //this just brings in jQuery
    "lib/*.js"
  ],
  tests: [
    //our test file (yet to be created)
    "*.test.js"
  ],
  //allows us to bring in our views without firing up a server
  //@see: http://www.kraken.no/blog/2012/03/23/injecting-html-with-busterjs/
  resources: [ { path: "/form", content: fs.readFileSync('form.html') } ]
};

Finally, on to the good stuff. We’ll write our test first of course.

form.test.js


buster.testCase('Login Form', {
  setUp: function(done) {
    var self = this;

    //this calls the "/form" route we set up in the "resources" section
    // of our config file above and reads in the HTML from "form.html"
    $('body').load(buster.env.contextPath + '/form', function() {
      //use jQuery to grab our form element
      self.$form = $('form#login');
      done();
    });
  },

  //the simple tests are always the best, make sure the form exists
  'exists': function() {
    //since jQuery represents selectors as arrays, this will always exist, so
    // we'll test the length to make sure there's an element in the selector
    assert(this.$form.length);
  }
});

To run our test, we type

$ buster test

…and we get something that looks like this…


PhantomJS 1.6.1, OS X: F
  Failure: PhantomJS 1.6.1, OS X Login Form exists
  [assert] Expected 0 to be truthy

Great! It failed. Tests should always fail when you write them. Now, the HTML to fill our test.

form.html


<form id="login"></form>

Some more tests


'method is POST': function() {
  assert.equals('post', this.$form.attr('method').toLowerCase());
},

'action is /login.php': function() {
  assert.equals('/login', this.$form.attr('action'));
},

'has a username input first': function() {
  var $usernameInput = this.$form.children(':first');
  assert.equals('INPUT', $usernameInput.get(0).tagName);
  assert.equals('username', $usernameInput.attr('name'));
}

And the HTML for those


<form id="login" action="/login" method="post"><input type="text" name="username" />

…repeat until the view is complete. Remember to test the actual submission of the form, client-side validation if it adds elements to the DOM, server error handling, etc.  Check out SinonJS for stubbing server requests.  It’s written by one of the authors of Buster so it has really nice integration. Trust me, you’ll have the best night’s sleep you’ve had in a while.  Now you’re confident that the form works when it’s loaded into a layout, or several layouts as is often the case with login forms.  What happens if it doesn’t work when it’s inserted into a layout?  Well, we’ve eliminated (or significantly mitigated) the possibility that it’s a problem with the form, so it must be a conflict with something else on the page.  For that we turn to full stack testing, which is a topic for another day.  I’m thinking live browser testing should be too.

Next time…

Yep, this post has gotten pretty long already so I’ll go into setting up and running live browser tests in my next post.

Have I missed anything in testing HTML that I should consider testing?  Is there a better methodology, framework, or tool that I should be using?  Let me know your thoughts in the comments below or on Twitter.


To keep up with my latest posts, please follow me on Twitter.

  • Pingback: Test-Driven HTML: Why and How | David Dripps « Sutoprise Avenue, A SutoCom Source

  • http://softwaregreenhouses.com Marty Nelson

    Photocopy testing is far simpler and more comprehensive for this type of testing. Simply establish a “standard” and compare that the same HTML is being produced.

    HTML is a declarative language whose intent/outcome is visual structure with some directive information. “” *is* the specification, it doesn’t add any value to parse and examine it using a procedural language.

  • http://softwaregreenhouses.com Marty Nelson

    haha, HTML was stripped out. “” *is* the specification refers to HTML:

    _form id=”login” action=”/login” method=”post”_ _input type=”text” name=”username” _

  • http://www.daviddripps.com David

    I’m not familiar with Photocopy testing. If it’s automated and it works, then I’m probably all for it.

    You make a good point about HTML being a declarative language. Honestly, if you’re not using layouts, templates, view helpers, etc. then there’s probably little to no value in TDDing your HTML. In my projects, however, there are definitely enough moving parts involved in assembling a HTML page that it warrants testing.

    When I finally get to my post on full-stack testing, I hope to illustrate how testing helps make modifying those view components a breeze.

    Also, there are benefits from test-driven development aside from the actual tests. In the early stages of a project, I take a somewhat abnormal approach and write as many deferred tests as I can think of. This helps me understand how that piece of code works and relates to other objects in the codebase. Kinda like writing a requirement spec on the fly.

    Thanks for the response. If you get a chance send me some links on photocopy testing. I always love to learn something new.

  • Pingback: Test-Driven HTML Part 2: Head Is Good | David Dripps