Unit testing in AsciiDoc

While finishing off this book, which I choose to write in AsciiDoc format, I really appreciate the time I took to write myself a handy little script for testing the little code examples. Because, something always gets wrong in the final steps of editing and is nice to have some automated assurance that you didn't break something stupid.

AsciiDoc or MarkDown or some other text setup is how technical books should be written. Or any books, really. Having clear plain text lets you focus on the content and not wrestle with Word formatting. And it's also easy to parse and test.

Code in AsciiDoc

The code blocks in AsciiDoc are easy to spot. They look like:

[source,js]
----------------------------------------------------------------------
var a = 0xff;
a === 255; //:: true
----------------------------------------------------------------------

Testing and linting

All you have to do in a little unit testing utility is to extract these clearly defined blocks and run them to make sure there are no silly syntax errors, creeping in in the very last edits.

But why stop with a syntax check? Why not also run jslint and also instrument and run the code and see any expected values. Asserts, in testspeak.

In my setup I did just that: lint with jshint, instrument the places where I know the expected values, lint these too (why the hell not?), run them and assert the expected values.

Output

Here's how the output of the utility looks like:

$ node scripts/test.js 
passed: 362, skipped: 43
linted: 317, nolints: 45

Since this is a book with some educational bad examples, not everything can be linted. Or run for that matter.

JSHint

I choose JSHint over JSLint as it allows more flexibility to relax the rules. Also it's a node module I can import.

var jslint = require('jshint').JSHINT;

These are my lint options:

// JSHint options need to be more relaxed because the book also
// points out bad patterns
var lintopts = {
  indent: 2,   // 2 spaces for indentation
  trailing: true, // disallow spaces at the end of a line
  white: true,
  plusplus: false, // allows ++ and --
  browser: true,   // assumes some common browser globals exist
                   // such as `document`
  node: true,  // assumes the code can run in node.js
               // and globals such as `global` are defined
  expr: true,  // ok to have expressions that seemingly do nothing
               // such as `a; // true` which the samples use to show
               // result values
  loopfunc: true, // allows definition of a function in a loop
                  // for educational purposes (in the part about closures)
  newcap: false,  // allows calling constructors (capitalized functions) 
                  // without `new`, again just for educational purposes
  proto: true,    // allows using `__proto__` which is great for understanding
                  // prototypes, although it's not supported in all browsers
};

And the lint function that lints a snippet of code:

// lint a snippet
function lint(snip) {
  // lint the snippet with all the options and have it assume
  // assert objext exists
  if (!jslint(snip, lintopts, {assert: false})) {
    log('------');
    log(snip);
    log('------');
    log(jslint.errors[0]);
    process.exit();    
  }
}

Instrumenting and executing a snippet

Execution is simple:

// run a snippet
function exec(snip) {
  // muck some stuff up and zap log()
  var mock = "function define(){}; function alert(){}; console.log = function(){};";
  try {
    eval(snip + mock);
    passed++;
  } catch (e) {
    log('------');
    log(snip);
    log('------');
    log(e.message);
    process.exit();
  }
}

The instrumentation is a little more interesting.

I often write snippets like:

// assign
var a = 1;
// test
a; // 1

Now I need a little bit of marker that will tell the instrumentation that a; is a piece of code to execute and 1 is the expected value. I came up with //::

So the above becomes:

var a = 1;
a; //:: 1

I also like to add some more explanation besides the returned value. I use ,, for that. So:

var a = 1;
a; //:: 1,, as you'd expect

Instrumented, this becomes:

var a = 1;
assert.deepEqual("a;",  1, "Error line #2");

The line is the line in the book with all code and prose, so it's easy to find.

The special markup like //:: and ,, gets stripped by another script that I run before commit.

A few other features are: support for NaN which doesn't deepEqual to anything and expecting errors with assert.throws(..)

So I can write and test code like:

sum(21, 21); //:: 42
plum(21, 21); //:: Error:: plum() is not defined

(:: that follow Error is the same as ,,)

Also:

Number("3,14"); //:: NaN

So here's the code instrumentation:

// Add asserts
function prep(l, n) {
  var parts = l.split(/;\s*\/\/::/); // "//::" separates expression to execute from its result
  var nonspace = parts[0].match(/\S/);
  var spaces = nonspace === null ? "" : Array(nonspace.index + 1).join(" ");
  parts[0] = parts[0].trim();
  if (parts[1]) {
    var r = parts[1].split(/\s*(,,|::)\s*/)[0].trim(); // the result may have ,, or ::, ignore what's on the right
                                                       // e.g. //:: true,, of course!
                                                       // e.g. //:: ReferenceError::Invalid whatever
    if (r.indexOf('Error') !== -1) {
      // expect //:: Error to throw
      return spaces + 'assert.throws(function () {' + parts[0] + '; }, ' + r + ', "error line #' + n + '");';
    }
    if (r === 'NaN') {
      // special NaN case
      return spaces + 'assert(isNaN(' + parts[0] + '), true, "error line #' + n + '");'
    }
    // usual
    return spaces + 'assert.deepEqual(' + parts[0] + ', ' + r + ', "error line #' + n + '");';
  }
  return l;
}

Main

Dependencies, locals, options and the main parser loop is how it all begins/ends:

// dependencies where I can see them
var assert = require('assert');
var fs = require('fs');
var jslint = require('jshint').JSHINT;
 
// buncha local vars
var snip, rawsnip.....;
 
// short
var log = console.log;
 
 
// JSHint options 
var lintopts = {
  indent: 2,   // 2 spaces for indentation
  // ....
};
 
// read the book one line at a time
fs.readFileSync('book.asc').toString().split('\n').forEach(function(src, num) {
 
  // src is a line in the book
  // num is the line number
 
});

There are a few additional features at snippet-level:

Ability to continue from a previous snippet using --//-- at the top of the snippet delimiter

Let's declare a variable:

[source,js]
----------------------------------------------------------------------
var a = 1;
----------------------------------------------------------------------

And then another one:

[source,js]
--------------------------------------------------------------------//--
var b = 2;
----------------------------------------------------------------------

And let's sum

[source,js]
--------------------------------------------------------------------//--
a + b; //:: 3
----------------------------------------------------------------------

Ability to skip a non-working snippet using ////

[source,js]
----------------------------------------------------------------------////--
var 1v;  // invalid
----------------------------------------------------------------------

Ability to run in non-strict mode (because strict is default) using ++

[source,js]
----------------------------------------------------------------------++--
var a = 012;
a === 10; //:: true
----------------------------------------------------------------------

nolint option

[source,js]
----------------------------------------------------------------------
/*nolint*/
assoc["one"]; //:: 1
----------------------------------------------------------------------

Cleanup before commit

Cleaning up all instrumentation markers and hints for the lint and the tests (gist):

var clean = require('fs').readFileSync('book.asc').toString().split('\n').filter(function(line) {
  if (line.indexOf('/*nolint*/') === 0 || line.indexOf('/*global') === 0) {
    return false;
  }
  return true;
})
  .join('\n')
  .replace(/--\+\+--/g, '--')
  .replace(/--\/\/--/g, '--')
  .replace(/--\/\/\/\/--/g, '--');
 
console.log(clean);

Github gist

Here's the test.js script in its entirety.

8 Responses to “Unit testing in AsciiDoc”

  1. JSSpy » Unit testing in asciidoc Says:

    [...] Source: http://www.jspatterns.com/unit-testing-in-asciidoc/ [...]

  2. Technical writing checklist @ Stoyan Stefanov Says:

    [...] I'm writing about JavaScript, JSLint (or JSHint) are constant companions. Consistency (semi-colons, spacing) helps the reader focus on important things. Plus, sloppiness in [...]

  3. scholarship info Says:

    I am really impressed together with your writing skills as neatly as with the layout to your blog. Is that this a paid theme or did you modify it yourself? Either way stay up the excellent high quality writing, it’s rare to see a great blog like this one nowadays..

  4. Ed Mominee Says:

    Good quality along with high-class. Kit is just as proven.

  5. canada goose mens jackets clearance Says:

    Also, if you study a preceding publish of mine,this document appears to demonstrate who I think is the perfect prospect for your Father of Daniel Holifield/Holyfield who married Mary Pye. This exact same son of Canada Goose Ontario Parka Women Valentine Holyfield,named Daniel appears to potentially be exactly the same Daniel Holifield who aids mark boundaries and guide study land belonging to John Mercer in 1741, in Canada Goose Coats Price Southern Virginia. And also seemingly the same Daniel H

  6. js4php: thanks! | Dring Drong Says:

    [...] every time. Takes a while, you get distracted by something shinier and so on… [...] Unit testing in AsciiDoc While finishing off this book, which I choose to write in AsciiDoc format, I really appreciate the [...]

  7. BrorieTurge Says:

    Studies also demonstrate that youngsters with autism undergoing routine CST can produce more a feeling of trust thereby leading to consistent eye contact. Create worthwhile content material that reliable organizations will obtain beneficial and worthy.When you have a damaged glass within your areas, the ideal thing you can do is [url=http://www.thejerseysseahawks.com/Super-Bowl-12th-Fan-Womens-Jersey/]Elite 12th Fan Super Bowl Jersey[/url] to get the attention of glass repairs Sydney. mHealth applications make use of sensors that can track wellness metrics, these as heart amount, blood pressure, glucose checking, medicine thresholds and more. You could also pick the get the companies of a Facebook marketing company that will tailor match every single essential aspects needed for initiating Facebook Management for your business. Keep reading to find out the road to better savings on [url=http://www.thejerseysseahawks.com/Super-Bowl-Bobby-Wagner-Womens-Jersey/]Elite Bobby Wagner Super Bowl Jersey[/url] your expenses. Operate an electronic toothbrush to eliminate marks that stick to the exterior of the teeth. Check that the fitted sheets you purchase correctly fit the size of your crib mattress, and get at least 3-4 extra sheets so you have a good supply for [url=http://www.thejerseysseahawks.com/Super-Bowl-Sidney-Rice-Womens-Jersey/]Sidney Rice Super Bowl Jersey[/url] changes especially during babies early months when they can be expected to make a lot of messes.

  8. http://lechnerrealty.com/shop/cheapmichaelkors.asp Says:

    The elf stopped and glared. “I’m fore warning you,” the elf said, his eyes wide and red as stoplights, “your mouth can be a weapon of X mass destruction, and Santa doesn’t be thankful one bit. No siree.” “But surely,” Samuel stammered, “there are better ways of saying than kidnapping me and stapling me towards wall.” “Better ways, he says,” the elf said, beginning to pace again. Chris Parker, executive director of Sprout Utah, and Paige Pitcher, project director, said the Michael Kors Outlet far

Leave a Reply