Sunday, 21 July 2013

JavaScript Testing Overload!!

Arent' you a lucky bunch this month? Following on from yesterday's blog post, I continued my foray into mocha by using different assertion libraries today.

I started with Chai, which is an assertion library that follows on from the assert node module by introducing its own assert syntax, but it also introduces the 'expect' and 'should' BDD syntax into the mix. Before delving into that, a little recap of assert may be in order.

Node: Assert

Straightforward assertion library which can be nicely tied in to Jasmine syntax. The basic assertions for the calculator app took the form of:

assert = require("assert");
 
Calculator = require("../Calculator.js").Calculator
describe("A calculator"function () {
    describe("adding 3 and 4 together"function () {
        it("should return 7"function () {
            var result = new Calculator().AddNumbers(3, 4);
            assert.equal(7, result, "But the number " + result + " was returned instead");
        });
    });
 
    describe("aubtracting 3 from 5"function () {
        it("should return 2"function () {
            var result = new Calculator().SubtractNumbers(5, 3);
            assert.equal(2, result, "But the number " + result + " was returned instead");
        });
    });
 
    describe("multiplying 7 and 8"function () {
        it("should return 56"function () {
            var result = new Calculator().MultiplyNumbers(7, 8);
            assert.equal(56, result, "But the number " + result + " was returned instead");
        });
    });
 
    describe("dividing"function () {
        it("8 by 4 should return 2"function () {
            var result = new Calculator().DivideNumbers(8, 4);
            assert.equal(2, result, "But the number " + result + " was returned instead");
        });
 
        it("7 by 2 should return 3.5"function () {
            var result = new Calculator().DivideNumbers(7, 2);
            assert.equal(3.5, result, "But the number " + result + " was returned instead");
        });
 
        it("5 by 0 should throw an exception"function () {
            assert.throws(function () { new Calculator().DivideNumbers(5, 0); }, Error, "This did not throw the expected error");
        });
    });
});

The key is to note that each assertion roughly takes the form of:

assert.<function>(<expected>,<actual>[,<message>]);

The main deviations from this format include when using exceptions, failing, checking for existence etc.

When using other libraries, you find that there are some subtle or significant differences.

Chai: Assert

To run the chai examples, you need to install chai node modules which can be pulled from npmjs.org by issuing the usual:

npm install chai

Being a lazy so and so, I wanted to change as little code as possible. When using the chai assert syntax for the calculator tests, you'll notice that it is somewhat the same. The main difference is catching exceptions which is done with a slightly different prototype.

// assert = require("assert");
assert = require("chai").assert;  // Note that I left the old assert require commented out to
// show how it differs.
Calculator = require("../Calculator.js").Calculator
describe("A calculator"function () {
    describe("adding 3 and 4 together"function () {
        it("should return 7"function () {
            var result = new Calculator().AddNumbers(3, 4);
            assert.equal(7, result, "But the number " + result + " was returned instead");
        });
    });
 
    describe("subtracting 3 from 5"function () {
        it("should return 2"function () {
            var result = new Calculator().SubtractNumbers(5, 3);
            assert.equal(2, result, "But the number " + result + " was returned instead");
        });
    });
 
    describe("multiplying 7 and 8"function () {
        it("should return 56"function () {
            var result = new Calculator().MultiplyNumbers(7, 8);
            assert.equal(56, result, "But the number " + result + " was returned instead");
        });
    });
 
    describe("dividing 8 by 4"function () {
        it("should return 2"function () {
            var result = new Calculator().DivideNumbers(8, 4);
            assert.equal(2, result, "But the number " + result + " was returned instead");
        });
    });
 
 
    // Note the exception expectation is slightly different in Chai to node assert.
    describe("dividing 5 by 0"function () {
        it("should throw an exception"function () {
            // The throw in the next line in particular operates very differently to Node's assert
            /* It doesn't process the infinity call the same as assert */
            assert.throws(function () { new Calculator().DivideNumbers(5, 0) }, Error, "Attempt to divide by zero!");
        });
    });
});

Not much different, so learning the first method covered off the second. BDD-like syntax is somewhat different.

Chai: Should

If you have not done so already, install the chai module (if you have followed assert above, then you will already have should in your node_modules directory in your local folder).

Being  a BDD template, should takes the form of

<ItemUnderTest>.should.<comparator>(<value>)[.<otherchainfunction>]

So for the AddNumbers test, it looks like:

 result.should.equal(7);

Note that both Should and expect use a chainable language to evaluate their tests, which means that the functions can run multiple evaluations in one statement chain. For example, testing for an exception and testing that it is the right string. I have modified this version of the tests to show this in action. I have also called the should() function after requiring it.

var should = require("chai").should();
 
Calculator = require("../Calculator.js").Calculator
describe("A calculator"function () {
    describe("adding 3 and 4 together"function () {
        it("should return 7"function () {
            var result = new Calculator().AddNumbers(3, 4);
            result.should.equal(7);
        });
    });
 
    describe("subtracting 3 from 5"function () {
        it("should return 2"function () {
            var result = new Calculator().SubtractNumbers(5, 3);
            result.should.equal(2);
        });
    });
 
    describe("multiplying 7 and 8"function () {
        it("should return 56"function () {
            var result = new Calculator().MultiplyNumbers(7, 8);
            result.should.equal(56);
        });
    });
 
    describe("dividing 8 by 4"function () {
        it("should return 2"function () {
            var result = new Calculator().DivideNumbers(8, 4);
            result.should.equal(2);
        });
    });
 
 
    describe("dividing 5 by 0"function () {
        it("should throw an exception"function () {
            // The throw in the next line in particular operates very differently to Node's assert
            var refFn = function() { new Calculator().DivideNumbers(5, 0); };
 
            // Note the following chained calls to determine the error class AND the message call. It evaluates them one by one.

            refFn.should.throw(Error).and.throw('Attempt to divide by zero!');
        });
    });
});


Conclusion

This short intro shows some examples of other JS assertion libraries. There are subtle differences between them and what you choose will depend on what you hope to accomplish and how familiar your developers are with the syntax. If I was introducing this into a team lacking BDD skills and no intent to use them, with a lack of familiarity with node, I would introduce the default assert libraries first, since this would require the least time to become productive from where they are.

Should you wish to eventually introduce BDD into the team, then Chai offers a good alternative and has the greatest scope for expansion without having to relearn any new assertion libraries. The team begin to use the chai assert style and migrate to using should or expect through retrospectives.

If the team are already familiar with BDD syntax and processes and have no problem understanding chaining, then I would introduce chai from the start as the learning curve is not as steep (or to be exact, they are further up it).

In all cases though, if you cant find learning resources for it, then flag this up as a major debt issue, since you will need to repay that to allow the team to scale effectively. For example, by team members keeping internal blogs or wikis with the validated learning that has taken place. Otherwise the team risks stumbling when it finds it needs to scale, introducing wasted time and risk into the flow.

0 comments:

Post a Comment

Whadda ya say?