Node.JS: Testing your Module

In my last post, I had explained how to create a NodeJS module. Along with creating a new module, testing is an integrate part of code develop. So I would like to outline my experience with developing a less-compiler-plugin.

Using Vows for Testing
Vows made black box testing really easy. Unlike JUnit for Java, only an object map with the tests are necessary to run the tests. No more creating dead code to create a method with an @Test annotation. Also, there is no concept such as @AfterClass or @BeforeClass as the JavaScript code is placed in the appropriate area to be run. Basically, my arguments boil down to the flexibility of using a scripting language rather than a compile one.

As the Vows project page’s description of using the framework is fairly comprehensive, I am going to skip that mundane topic to move on to coverage testing.

Using Istanbul for Coverage Testing
When I had first done testing, it seemed like tools such as Istanbul was a method of running coverage tests. It really boiled down to two commands.

istanbul cover [test file]
istanbul report

Testing less-compiler-plugin for YUI Shifter
Using Vows and Istanbul together was a pleasant, especially vows for its minimalist architecture. When writing my tests, I ran into the issue that my module’s lib code was sand-boxed, so I could not access some inner functions. To fix this problem, I added exports for all the functions inside the `lib/index.js` file.

/*jshint forin: false */
var fs = require('fs'),
    path = require('path'),
    less = require('less'),
	fileExtention = '.less',
	encoding = {encoding: 'utf-8'};

/**
* Finds all the LESS files.
*
* @param baseDir String path to search.
* @protected
*/
var findLessFiles = function(baseDir) {
	var files = fs.readdirSync(baseDir),
	foundFiles,
	lessFiles = [];

	// find all less files.
	files.forEach(function(file) {
		var filePath = path.normalize(path.join(baseDir, file)),
		stat;
		if (path.extname(file) === fileExtention) {
			lessFiles.push(filePath);
		}
		else {
			stat = fs.statSync(filePath);
			if (stat && stat.isDirectory()) {
				foundFiles = findLessFiles(filePath);
				foundFiles.forEach(function(foundFile) {
					lessFiles.push(foundFile);
				});
			}
		}
	});
	return lessFiles;
};

/**
* Write the compiled CSS to disk. 
*
* @param compileError Any error when compiling.
* @param css Compiled CSS String.
* @protected
*/
var writeCompiledCSSToDisk = function(compileError, css) {
	var cssFile = this.filePath.replace('.less', '.css'),
	diskError;
	if (compileError) {
		console.log('[err] Failed to compile: ' + this.filePath);
		console.log('[stack] ' + compileError.message);
		console.log('[stack] ' + compileError.stack);
	}
	else {
		diskError = fs.writeFileSync(cssFile, css, encoding);
		if (diskError) {
			console.log('[err] Failed to write compiled CSS to '
                                + this.filePath);
			console.log(diskError);
		}
	}
};

/**
* Recursively compiles all less files.
*
* @param baseDir File path as a String.
* @protected
*/
var run = function(baseDir) {

	console.log('[info] Compiling less files in: ' + baseDir);

	if (!baseDir || !fs.existsSync(baseDir)) {
		console.log("[warn] Base directory was invalid: "
                        + baseDir);
		return false;
	}

	// find all less files
	var lessFiles = findLessFiles(baseDir);
	if (lessFiles.length === 0) {
		console.log("[warn] No LESS files were found");
		return false;
	}

	// compile the file.
	lessFiles.forEach(function(filePath) {
		console.log('[info] Compiling file: ' + filePath);
		var data = fs.readFileSync(filePath, encoding),
		obj = {
			"filePath": filePath,
			"handler": writeCompiledCSSToDisk
		};
		less.render(data, function(error, css) {
			obj.handler(error, css);
		});
	});

	return true;
};

// used for testing.
exports.findLessFiles = findLessFiles;
exports.writeCompiledCSSToDisk = writeCompiledCSSToDisk;

// public function.
exports.run = run;

After fixing the exports, I was able to successfully test the code.

var vows = require('vows'),
	fs = require('fs'),
	path = require('path'),
        assert = require('assert'),
	compiler = require('../lib/index.js'),
	encoding = {encoding: 'utf-8'};

var tests = {
	'find all the assets': {
		topic : function() {
			var baseDir = './tests/assets/';
			return compiler.findLessFiles(baseDir);
		},
		'find all the assets': function(arr) {
			var expected = {
				'./tests/assets/file1.less': true,
				'./tests/assets/skin/sam/file2.less': true,
				'./tests/assets/skin/sam/file3.less': true
			};

			for (var index in expected) {
				expected[path.normalize(index)] = true;
			}

			assert.isTrue(arr.length === 3);
			for (var index in arr) {
				assert.isTrue(expected[arr[index]]);
			}
		}
	},
	'no less files exists': {
		topic : function() {
			return compiler.findLessFiles('./lib/');
		},
		'no less files exists': function(arr) {
			assert.isTrue(arr.length === 0);
		}
	},
	'Write file' : {
		topic : function() {
			var error = null,
			compiledCSS = '.foo{color:red;}',
			obj = {
				"filePath": path.normalize("./tests/writeTest.less"),
				"handler": compiler.writeCompiledCSSToDisk
			};
			try {
				obj.handler(error, compiledCSS);
			}
			catch(error) {
				console.log(error);
				return null;
			}

			// return dummy value.
			return obj.filePath;
		},
		'Write file' : function(compiledFile) {
			var error, compiledData;
			assert.isNotNull(compiledFile);

			compiledFile = compiledFile.replace('.less', '.css');
			assert.isTrue(fs.existsSync(compiledFile));

			compiledData = fs.readFileSync(compiledFile, encoding);
			assert.isTrue(compiledData.length > 0);
			assert.isFalse(compiledData === 'undefined');
		}
	},
	'Cannot compile null directory': {
		topic: function() {
			return compiler.run(null);
		},
		'Cannot compile null directory': function(isCompiled) {
			assert.isFalse(isCompiled);
		}
	},
	'No directory passed as a parameter': {
		topic: function() {
			return compiler.run();
		},
		'No directory passed as a parameter': function(isCompiled) {
			assert.isFalse(isCompiled);
		}
	},
	'No LESS files found': {
		topic: function() {
			return compiler.run('./lib/');
		},
		'No LESS files found': function(isCompiled) {
			assert.isFalse(isCompiled);
		}
	},
	'Compile the less files': {
		topic: function() {
			return compiler.run('./tests/assets/');
		},
		'Compile the less files': function(isCompiled) {
			assert.isTrue(isCompiled);
		}
	}
};

vows.describe('general').addBatch(tests).export(module);

Issue with line endings CRLF versus LF
It was peculiar that I ran into a line ending issue when running my testing code on OSX since NodeJS is cross platform. To fix the line ending problem, I forced Unix line endings on all the text files using .gitattributes.

* text eol=lf

*.js text eol=lf