7
\$\begingroup\$

I decided to port my favorite unit testing framework, Catch, to JavaScript. So far I'm focusing on test cases and sections. I'll add assertions in phase 2, and output in phase 3.

The goal for this phase was to get all the assertions in the following test code to pass:

function assert(value, message) {
    console.log(value ? 'pass' : 'FAIL', message || '')
}

TEST_CASE('See if this works right', '[stuff]', function(){
    var x = 2, y = 4;

    console.log('============');
    SECTION('halving a good time', function(){
        x /= 2;
        y /= 2;
        assert(x == 1 && y == 2, 'halved values');
    });
    SECTION('double the fun', function(){
        x *= 2;
        y *= 2;
        assert(x == 4 && y == 8, 'doubled values');
        SECTION('up high', function(){
            x += 5;
            y += 5;
            assert(x == 9 && y == 13, 'high five');
        });
        SECTION('down low', function(){
            x -= 5;
            y -= 5;
            assert(x == -1 && y == 3, 'low five');
        });
    });
});

Notice that the entire test case should be run from the beginning, once for each "leaf" section, just like Catch (follow the second link in this question for a more complete explanation).


Here's what I've written so far. The documentation is very minimal right now; I'll be annotating and commenting properly later.

( /** @type function(Window) **/ function(global) {
    'use strict';

    var context = [],
        testCases = [],
        currentTestCase,
        nextTask;

    /** Section.
    */
    function Section(name, body, tags) {
        this.name = name;
        this.tags = tags;
        this.body = body;
        this.subsections = [];
        this.subsectionIndex = 0;
        this.subsectionsEncountered = 0;
    }

    /** Run section.
    */
    Section.prototype.run = function() {
        context.unshift(this);
        this.subsectionsEncountered = 0;
        this.body();
        context.shift();
    }

    /** Update subsection.
    */
    Section.prototype.updateSubsection = function(name, body) {
        var index = this.subsectionIndex,
            subsection = this.subsections[index];

        if (!subsection) {
            subsection = new Section(name, body);
            this.subsections[index] = subsection;
        } else {
            subsection.body = body;
        }

        return subsection;
    }

    /** Test case.
    */
    global['TEST_CASE'] = function(name, tags, body) {
        var next;

        currentTestCase = new Section(name, body, tags);
        testCases.push(currentTestCase);
        currentTestCase.run();
        while (nextTask) {
            next = nextTask;
            nextTask = null;
            next();
        }
    };

    /** Test case section.
    */
    global['SECTION'] = function(name, body) {
        var outer = context[0],
            oldNext = nextTask;

        if (outer.subsectionIndex == outer.subsectionsEncountered++) {
            nextTask = function() {
                if (++outer.subsectionIndex < outer.subsectionsEncountered) {
                    currentTestCase.run();
                } else if (oldNext) {
                    oldNext();
                }
            };
            outer.updateSubsection(name, body).run();
        }
    };

}(this));

I've kept the macro-style naming conventions from Catch, and dumped a bunch of things in the global scope. This may or may not change, but this is one thing I'm probably not interested in having reviewed, since it's easier for me to keep the names exactly the same while porting. The rest is fair game.

Update: phase 2 is mostly complete.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Overall Design

For such a small amount of code, I find the logic extremely difficult to follow. I understand what is happening at a high level, but it seems every function is responsible for managing everything, and program control bounces around a lot.

As an example, the final function assigned to SECTION has several responsibilities:

  1. Capture the enclosing section and current next task.
  2. Do nothing if this new section should be skipped.
  3. Create a new next task that will either
    • rerun the test case (if more sections have been added),
    • run the previous next task (if there is one), or
    • do nothing.
  4. Add the new section to the enclosing section.

    BTW, under what condition does updateSubsection not create a new section? I can see it looks for a section with the same name, but is that supposed/allowed to happen?

  5. Finally, run the new section.

That's way too much work for one nine-line function to do.

As a first step, add a TestCase class (subclass of Section) to own the overall state of running a single test case, and move the logic from the last two functions into it, breaking them up into smaller functions. While test cases are run sequentially, I would move context and nextTask into this new class as I believe they belong to a single test case run.

Names

Most of the variable names are pretty clear, but as I dig into the code some are misleading.

  • context sounds more like a map of values passed to each section rather than a stack of running sections. I would rename it to reflect that nature, e.g., activeSections.

  • subsectionsEncountered is a little cumbersome; how about the more standard subsectionCount? And subsectionIndex only tells me it's an index into some container--not why that subsection is special. I'd go with activeSubsectionIndex though I would prefer to store the actual Section instance in it and drop the Index suffix if possible.

  • You're creating a tree of sections, and parent is more obvious than outer.

Other Thoughts

I'd be tempted to drop the stack of sections (context) in favor of storing just the active section since that's all you need. To pop a completed section off the stack, a) store a reference to the parent section in each section instance and b) assign the parent to activeSection after each section completes.

Finally, you need to handle exceptions so other sections can be run. You should be able to do it once in Section.prototype.run.

On to Phase Two!

I'm very curious to see this in action. With QUnit I am often forced to perform the same actions on the fixture as a previous test did in order to set up the current test. This causes code duplication whereas the whole point of setting up a fixture is to avoid it.

In Java I've been building separate fixture classes that provide methods for setting up common fixture states that can be reused across many tests. While that's helpful, it's not as easy to do in JavaScript.

\$\endgroup\$
7
  • \$\begingroup\$ "That's way too much work for one nine-line function to do." -- can you give a (working) example of how you would refactor this? BTW updateSubsection does not create a new section when re-visiting a section to find new leaves. \$\endgroup\$
    – Dagg
    Jul 4, 2014 at 22:51
  • \$\begingroup\$ hmm, maybe sectionStack instead of context? \$\endgroup\$
    – Dagg
    Jul 4, 2014 at 22:58
  • \$\begingroup\$ subsectionsEncountered because the count resets every time we revisit a section, so it encounters them all again on each iteration... so "count" felt wrong (I considered it when writing this). I like activeSubsectionIndex though. \$\endgroup\$
    – Dagg
    Jul 4, 2014 at 22:59
  • \$\begingroup\$ Considered parent too, seemed like 6 and half a dozen. outer speaks more to the way the test code is laid out than how the data is stored internally, which I think I like, for now. \$\endgroup\$
    – Dagg
    Jul 4, 2014 at 23:01
  • \$\begingroup\$ "drop the stack of sections" -- that could work. Originally I had a guard in there to terminate the thing if the stack got too big, as I was making stupid mistakes and hanging the browser while writing it. \$\endgroup\$
    – Dagg
    Jul 4, 2014 at 23:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.