The Test

Previously

This article continues my effort to explore the notion that one can develop software without being an actual developer as long as they are good at Googling.

That, of course, is a paraphrase of the question posed to Scott Hanselman who wrote an article in response. Since I’m curious about this idea, I’ve decided to document my Googling while developing an approach to one of the kata's mentioned in the Hanselman article.

Test First

I’ve yet to reach the point where I’m truly doing test-driven development. I often use the term “test-centered” when talking about my approach. You’ll notice in the first draft of my kata solution that there isn’t a test, i.e. I didn’t follow the instruction to write the unit test first. As penance and to fully embrace iterative software development, I am refactoring my code and creating a test to verify its functionality.

The first thing I did was research unit testing in Node.js since that’s my environment. This led me to a nice step-by-step on the codementor site for unit testing with Mocha and Chai. I’ve added a TODO item to compare other testing frameworks but, for now, the Starbucks stuff suffices.

Here’s my implementation of the kata’s test:

var chai = require('chai');
var t = {
    expect : chai.expect, 
    testDeps : function(){
        describe('Dependencies', function(){
           it('Test dependency graph', function(){
               var deps = require('./../src/transdep');
               deps.addDependency('A',['B','C']); 
               deps.addDependency('B',['C','E']); 
               deps.addDependency('C',['G']); 
               deps.addDependency('D',['A','F']); 
               deps.addDependency('E',['F']); 
               deps.addDependency('F',['H']); 
                                  
               t.expect(deps.getDependencies('A')).to
                .deep.equal(['B','C','E','G','F','H']);
               t.expect(deps.getDependencies('B')).to
                .deep.equal(['C','E','G','F','H']);
               t.expect(deps.getDependencies('C')).to
                .deep.equal(['G']);
               t.expect(deps.getDependencies('D')).to
                .deep.equal(['A','F','B','C','E','G','H']);
               t.expect(deps.getDependencies('E')).to
                .deep.equal(['F','H']);
               t.expect(deps.getDependencies('F')).to
                .deep.equal(['H']);
            });
        });
    }
}
t.testDeps();

Refactor

Attentive readers will notice my deviation from the kata. Specifically, the kata test expects a sorted dependency list. I’ll tackle that requirement in my next article. For now, let’s pretend I’ve convinced QA that the dependency order doesn’t matter. The order is deterministic so… On to the refactored code.

The test expects the dependency graph to have a way to add dependencies and to retrieve dependencies (direct and indirect) for a given object. The original code has this functionality living in the readDataFromCli and graphDependencies functions (highlighted below).

readDataFromCli: function(){
    process.stdin.resume();
    process.stdin.setEncoding('utf8');
    process.stdin.on('data', function(line) {
        var data;
        var i = 1;
        if (line === 'q\n') { 
           app.graphDependencies(); 
        } else {
            data =  app.processLine(line);
            app.deps[data[0]] = [];
            for(; i < data.length; ++i){
                app.deps[data[0]].push(data[i]);
            }
        }  
    });
}
graphDependencies: function (){
    console.log('Original Chart:');
    console.log(app.deps);
    for(var aKey in app.deps){
        for(var bKey in app.deps ){
            if(aKey !== bKey && 
                app.deps[aKey].includes(bKey)){
                app.deps[aKey] = app.concatDeps
                                (aKey,
                                 app.deps[aKey],
                                 app.deps[bKey]);
            }
        }
    }
  
    console.log('Dependency Graph:');
    console.log(app.deps);
    process.exit();
}

Moving the highlighted code into their own functions looks like this:

   addDependency : function(name, dArray){
        if(!deps[name]) deps[name] = [];
        deps[name] = util.concatDeps
                    (name,
                     deps[name],
                     dArray);
    }

    getDependencies : function(name) {
        var myDeps = []
        if(deps[name]){
            myDeps = deps[name].slice();
            for(var dKey in deps){
                if(myDeps.includes(dKey)){
                    myDeps = util.concatDeps
                            (name,
                             myDeps,
                             deps[dKey]);
                }
            }            
        }
        return myDeps;
    }

While refactoring the “add dependency” behavior I introduced support for calling addDependency() multiple times for an individual object. This provides a way to build up dependencies dynamically instead of defining them all at once.

I also split two of the “helper” functions into their own object instanced in the util variable. I did this for two reasons:

First, introducing client code (i.e. the test) made a public interface1 necessary and I want as lean and clean an interface as possible.

Second, it made sense to move generic functionality out of the dependency graph object. Plus, both the concatDeps() and the processLine() functions have high reusability potential.

Initially, I went to remove the concatDeps() function and use a Set to hold the dependencies. Thus removing the need for the manual array concatenation. However, some known issues with chai hinder that approach. I’ll keep an eye on their forum as this project progresses.

Here is the fully refactored code:

/* Since module.exports is needed for testing
 * Only include "public" functions in the exported object
 */
var deps = {};
var util = {
    concatDeps : function(key,a,b) {
        var arr = [];
        var i = 0;
        if(a.length === 0){
            arr = b.slice(0);        
        } else {
            for(; i < a.length; ++i){ arr.push(a[i]);}
            for(i = 0; i < b.length; ++i) {
                if(key !== b[i] && 
                  !arr.includes(b[i])){ 
                   arr.push(b[i]); 
                }
            }    
        }
        return arr;
    },    
    processLine : function (line){
        var depData = line.substring(0,line.length - 1);
        return depData.split(' ');
    }
};

/* This becomes the API */
module.exports = dGraph = {
    displayGraph : function (){
        var gDeps = {};
        console.log('Original Chart:');
        console.log(deps);

        for(var aKey in deps){
            gDeps[aKey] = 
             dGraph.getDependencies(aKey);
        }
  
        console.log('Dependency Graph:');
        console.log(gDeps);
    },
    addDependency : function(name, dArray){
        if(!deps[name]) deps[name] = [];
        deps[name] = util.concatDeps
                    (name,
                     deps[name],
                     dArray);
    },
    getDependencies : function(name) {
        var myDeps = []
        if(deps[name]){
            myDeps = deps[name].slice();
            for(var dKey in deps){
                if(myDeps.includes(dKey)){
                    myDeps = util.concatDeps
                            (name,
                             myDeps,
                             deps[dKey]);
                }
            }            
        }
        return myDeps;
    },
    readDataFromCli : function(){
        process.stdin.resume();
        process.stdin.setEncoding('utf8');
        process.stdin.on('data', function(line) {
            var data;
            var i = 1;
            if (line === 'q\n') {  
                dGraph.displayGraph(); 
                process.exit();
            } else {
                data =  util.processLine(line);
                dGraph.addDependency(data[0],data.slice(1))
            }  
        });
    }
};

Verify

The advice to write tests first cannot be overstated. As I said, I’m not there yet and doing this kata has reinforced my resolve to try harder. It’s certainly not easy. However, restructuring my code for the test raised questions I didn’t have to think about when creating the functionality in a silo, i.e. writing for a single use case. Answering those questions forced robustness and that is reason enough to start with the test first.

Throughout most of my career well-defined requirements have been elusive. Specifications are often amorphous at the beginning and sometimes, truth be told, written to match what’s been coded after the fact. Thinking about the unit tests during requirements gathering offers a focal point for identifying and solidifying specifications. When I’ve achieved the test being the specification I’ll buy myself a drink.

--
Randy


  1. I’m using this term generically to mean visible to other files. The approach I’m using comes from information I gathered here and here

Leave a Reply

Your email address will not be published. Required fields are marked *