# Catch-style unit testing in JavaScript (phase 2)

Continued from phase 1; please read it first for background.

# Overview

This phase focuses on assertions. Here's where things get ugly. Catch uses macro expansions to decompose assertion expressions. Since we don't have that luxury in JavaScript without adding a build-time requirement, we have to settle for Function.prototype.toString and eval. For example, take this simple assertion:

var x = 2;
REQUIRE(x == 3);


This assertion will fail, and when it does, it needs to be able to provide information about why it failed. It needs to tell us something like this:

FAILED: REQUIRE(x == 3), with expansion 2 == 3.

In order to do that, we need to transform the REQUIRE(x == 3) invocation into something like this (this is a slightly simplified example of what the current implementation would produce):

REQUIRE(x == 3, "x == 3", x + " == " + 3)


In other words, the REQUIRE function has a signature like the following, and invocations in test cases are transformed to match it.

function REQUIRE(result, expression, expansion) { ... }


# Restrictions

Fortunately, tests are meant to be run in a fairly controlled environment, and the issues that come from transforming a function to a string, doing some simple regex replacement, and transforming back to a function again can be avoided by following these simple rules:

• Test cases may only rely on values in the global scope. In other words, if the test cases themselves are not in the global scope, they should not reference anything in their local scope.

• Only one assertion is allowed per line, and only one line per assertion. Multiple assertions must not be placed on a line, and assertions must not span multiple lines.

• Assertion expressions should be kept simple. In particular, they must not include string literals containing anything that looks remotely like a comparison or logical operator.

The first rule cannot be avoided, as far as I can tell. The second two could be avoided by doing some proper parsing instead of regex replacement, but I'm not sure it's worth the code bloat -- those rules seem reasonable, and sensible workarounds exist.

# Source

The code from phase 1 has only been modified slightly. I'll probably post a final question with the complete source after all phases are complete.

Right now, only the REQUIRE and CHECK assertion macros are present. So far, I don't see a need for REQUIRE_FALSE and CHECK_FALSE in this port. I may add a "phase 2.1" for the exception handling macros if it's worth a separate post, otherwise I'll just leave it for the final question.

Here's the new stuff from this phase:

function Assertion(passed, expression, expansion, type) {
this.passed = passed;
this.expression = expression;
this.expansion = expansion;
this.type = type;
}

function assert(passed, expression, expansion, type) {
var outer = context[0],
assertion = new Assertion(passed, expression, expansion, type);

outer.assertions.push(assertion);

return assertion;
}

/** Require.

Test an expression and abort the test case if it fails.
*/
global['REQUIRE'] = function(result, expression, expansion, type) {
var assertion = assert(result, expression, expansion, type);

if (!result) {
// Not doing anything special with the exception yet,
// so just throw the assertion for now.
throw assertion;
}
};

/** Check.

Test an expression and continue execution of the test case.
*/
global['CHECK'] = function(result, expression, expansion, type) {
assert(result, expression, expansion, type);
};

//
// Preprocessing (rewrite test case functions)
//

var macroPattern = /(REQUIRE|CHECK)\s*$$(.*)$$/g,
operatorPattern = /\s*([=!<>]{2,3}|\|\||&&)\s*/g;

function quoteOperators(string) {
return ')+"' + string + '"+(';
}

function decompose(assertion) {
return '(' + assertion.replace(operatorPattern, quoteOperators) + ')';
}

function expand(string, functionName, assertion) {
var expression = JSON.stringify(assertion),
expansion = decompose(assertion),
args = [assertion, expression, expansion, JSON.stringify(functionName)];

return functionName + '(' + args + ')';
}

function rewriteTestCase(body) {
return ('(' + body + ')').replace(macroPattern, expand);
}


The TEST_CASE function has been modified to call body = eval(rewriteTestCase(body)), and error handling has been added to Section.prototype.run to catch thrown assertions.

Edit: in response to konjin's answer, renamed assertion to expression in a few places.

function decompose(expression) {
return '(' + expression.replace(operatorPattern, quoteOperator) + ')';
}

function expand(string, functionName, expression) {
var quotedExpression = JSON.stringify(expression),
expansion = decompose(expression),
type = JSON.stringify(functionName),
args = [expression, quotedExpression, expansion, type];

return functionName + '(' + args + ')';
}


## 1 Answer

From a design perspective, I think this is overkill, I assume you know what you're doing.

I looked at this code a fair bit, and it is well written, the only point is that

function decompose(assertion) {
return '(' + assertion.replace(operatorPattern, quoteOperators) + ')';
}


I would expect from a naming perspective that assertion is an instance of Assertion, but it clearly is a string. I am not sure what you should call the parameter.

• It feels like overkill, but the goal was to port Catch to JS, and AFAICT this kind of thing is my only option unless I want to add a build-time step and do true macros with M4 or similar. That assertion param was bothering me too. It's a string representing the expression passed to an assertion... I would have just called it "expression" except I used that for the quoted version of the same thing somewhere else. Maybe I'll change it to "expression" and change the other thing to "quotedExpression" or something. – Dagg Jul 8 '14 at 16:19
• You could also have an IDE macro at write time ? Or simply live without that extra information which you will get anyway when you debug the bug. KISS etc. Also, I would go for expression in both cases, better than assertion for sure. – konijn Jul 8 '14 at 17:10
• Hmm, maybe it could have two modes of operation to choose from, one where it rewrites the test cases and one where it leaves that info out. Something to think about for sure. – Dagg Jul 8 '14 at 17:36