Skip to content
Browse files

Vastly Improved Code Coverage

Code coverage using the cover method now gives
you a detailed look at which lines you missed
along with the appropriate number of surrounding
lines based on how many errors are located in a
block. Furthermore, code coverage can now happen
independantly of the test running portions of the
module.

* Added a callback to the run() method
* Fixed bug with non .js extensions in cover()
* Lazy loaded the coverage module
* Updated tests w/ coverage for stest using stest
  • Loading branch information...
1 parent 4e9017b commit ec05a3f43fa0a33c157c4900fbfe85c9a170929d Siddharth Mahendraker committed Jan 10, 2012
Showing with 163 additions and 70 deletions.
  1. +12 −9 README.md
  2. +129 −54 lib/stest.js
  3. +2 −2 test/test-coverage.js
  4. +20 −5 test/test-stest.js
View
21 README.md
@@ -1,8 +1,8 @@
# stest - A Sane Async Testing Framework
-`stest` is a fun, fast and simple testing framework
-particularly suited towards asynchronous code. It lets
-you easily structure tests for code with both
+`stest` is a fun, fast and simple testing framework
+particularly suited towards asynchronous code. It lets
+you easily structure tests for code with both
synchronous and asynchronous methods without too much
complexity.
@@ -24,10 +24,10 @@ A very simple test:
stest.addCase("stest", opts,{
setup: function(promise){
-
+
promise.emit("event", 42);
promise.emit("other_event", "Hello!");
-
+
mylib.async_func(function(err, obj){
promise.emit("async", err, obj);
});
@@ -54,16 +54,19 @@ corresponding functions associated with the name of the
events you've emitted.
The `setup` and `teardown` functions are given to you
-to setup your test case, and to perform a teardown.
+to setup your test case, and to perform a teardown.
`setup` is required, `teardown` is optional.
The `opts` argument allows you to specify a `timeout`
-in miliseconds. If all async calls are not called
+in miliseconds. If all async calls are not called
before that time, `stest` will give you a heads up.
`stest` also supports code coverage using the `cover`
method, which shows unseen LOC and gives you a brief
-overview of how much of the file you've tested.
+overview of how much of the file you've tested. Do note
+that `cover` may trip up if you have really whacky
+syntax (like assignments inside of conditional statements
+and such).
See the source code and inline documentation for more details.
@@ -81,7 +84,7 @@ Which looks like this in the command line:
srunner -r test/test-.*\.js
-If you prefer not to use `srunner`, you can
+If you prefer not to use `srunner`, you can
still run tests like this:
node test.js
View
183 lib/stest.js
@@ -2,7 +2,7 @@ var events = require("events"),
fs = require("fs"),
path = require("path"),
color = require("colors"),
- coverage = require("runforcover").cover(/.*/);
+ coverage = null;
/*
* @class stest
@@ -36,7 +36,7 @@ function stest(){
}
this._report = function(name, errors, time){
-
+
var result = "",
c = "green";
@@ -54,25 +54,87 @@ function stest(){
// print errs
errors.forEach(function(err){
self._out("red", err.message+"\n");
- if(err.type !== "stest")
+ if(err.type !== "stest")
self._out("red", err.stack+"\n");
});
}
+
+ this._reportCoverage = function(){
+ if(!coverage) coverage = function(){};
+
+ // show coverage data
+ coverage(function(cd){
+ self._coverqueue.forEach(function(lib){
+ var file = fs.readFileSync(lib).toString().split("\n");
+ var stats = cd[lib].stats();
+ var c = "magenta", err = "red", g = "green";
+
+ self._out("\n");
+ self._out(c, "Module: "+path.basename(lib)+"\n");
+ self._out(c, "Percentage seen: "+Math.floor(stats.percentage*100)+"%\n");
+ self._out(c, "Lines seen: "+stats.seen+"\n");
+ self._out(c, "Lines missed: "+stats.missing+"\n");
+
+ // this keeps track of the difference of lines between errors
+ // so that errors can be grouped according to proximity
+ var chainDiff = 0;
+ stats.lines.forEach(function(line, i, lines){
+ var lnum = line.lineno,
+ diff = lnum - chainDiff;
+
+ // if there's only a small gap between the lines,
+ // write the lines in between, or we have a break
+ if(diff < 10){
+ for(var t = 1; t < diff; t++){
+ var inv = diff - t;
+ self._out(g, (lnum-inv)+": "+file[(lnum-1)-inv]+"\n");
+ }
+ }else{
+ // if we aren't at the start, write the lines preceding
+ // the last break
+ if(chainDiff !== 0){
+ for(var e = 1; e < 5; e++)
+ self._out(g, (chainDiff+e)+": "+file[(chainDiff+e)-1]+"\n");
+ }
+
+ self._out("\n");
+
+ // write the lines before this new break
+ for(var q = 1; q < 5; q++){
+ var qdiff = 5 - q;
+ self._out(g, (lnum-qdiff)+": "+file[(lnum-1)-qdiff]+"\n");
+ }
+ }
+
+ self._out(err, lnum+": "+line.source()+"\n");
+ chainDiff = lnum;
+ });
+
+ if(chainDiff !== 0){
+ for(var p = 1; p < 5; p++)
+ self._out(g, (chainDiff+p)+": "+file[(chainDiff-1)+p]+"\n");
+ }
+ });
+ coverage.release();
+
+ self._out("\n");
+ });
+ }
}
/*
- * Adds a test cass to stest, along with some
+ * Adds a test cass to stest, along with some
* other acnillary stuff.
*
- * @param {String} name
+ * @param {String} name
* @param {Object} opts
* @param {Object} test
*
* The name will be displayed upon output.
*
* The opts object looks like this:
*
- * var opts = { timeout: 1000 }
+ * var opts = { timeout: 1000 }
*
* After the number of miliseconds specified by
* the timeout parameter, stest will throw an
@@ -82,37 +144,37 @@ function stest(){
*
* The test object looks something like this:
*
- * {
+ * {
* setup: function(promise){
* // emit events
* promise.emit("event_1", 42);
* },
* event_1: function(num){
* // catch events
- * assert.deepEqual(42, num);
+ * assert.deepEqual(42, num);
* },
* teardown: function(errors){
* // (optional)
- * // the errors object can
- * // be modified if you intend to
+ * // the errors object can
+ * // be modified if you intend to
* // create errors
* }
* }
*
* The setup key is required, all other keys and
* functions are optional. Errors in the teardown
* function have two extra properties; an event
- * property indicating the event that wasn't fired,
- * and a type, indicating weather the error was
- * generated by stest or not. stest errors have a
+ * property indicating the event that wasn't fired,
+ * and a type, indicating weather the error was
+ * generated by stest or not. stest errors have a
* type equal to "stest". For example:
*
* teardown: function(errors){
* // created two intentional errors earlier
* if (errors.length > 2) assert.ok(0);
* }
*
- * This method can also be chained to include
+ * This method can also be chained to include
* multiple cases, like so:
*
* stest.addCase(...,{
@@ -132,54 +194,66 @@ stest.prototype.addCase = function(name, opts, test){
}
/*
- * Allows stest to track function calls for
+ * Allows stest to track function calls for
* code coverage
*
* @param {String} lib
*
* This method returns the same object as
* require(lib) except it allows stest to track
- * which functions have been called and which
- * haven't.
+ * which functions have been called and which
+ * haven't.
*
* @api public
*/
-
stest.prototype.cover = function(lib){
+ if(!coverage) coverage = require("runforcover").cover(/.*/);
+
var realroot = path.dirname(module.parent.filename);
lib = path.resolve(realroot, lib);
+ if(path.extname(lib) == "") lib += ".js"
+
this._coverqueue.push(lib);
-
+
return require(lib);
}
/*
* Runs the test cases synchronously
*
+ * @param {Function} callback
+ *
* All output is written to stdout.
*
+ * The callback is called after all of the
+ * tests in the suite have been run and
+ * their timeouts have expired. Note that
+ * this is not a place to keep teardown
+ * code, the teardown method is more
+ * appropriate.
+ *
* @api public
*/
-stest.prototype.run = function(){
+stest.prototype.run = function(callback){
// clear context
this.ctx = {};
- var self = this;
+ var self = this,
+ timeoutsComplete = 0;
- this._queue.forEach(function(tcase){
-
- var test = tcase.test;
- var opts = tcase.opts;
- var name = tcase.name;
+ this._queue.forEach(function(tcase){
+ var test = tcase.test,
+ opts = tcase.opts,
+ name = tcase.name;
var setup = test.setup;
var teardown = test.teardown || function(){};
-
+
if(!setup) throw new Error("A setup method is required");
-
+
// to record elapsed time
var totalTime = 0;
// to record error
@@ -193,7 +267,7 @@ stest.prototype.run = function(){
Object.keys(test).forEach(function(event, i, array){
self.promise.once(event, function(){
var args = Array.prototype.slice.call(arguments, 0);
- totalTime += self._addTime(function(){
+ totalTime += self._addTime(function(){
try{
test[event].apply(self.ctx, args);
}catch(e){
@@ -221,37 +295,38 @@ stest.prototype.run = function(){
self.promise.removeAllListeners(event);
}
});
-
+
// user teardown
totalTime += self._addTime(function(){ teardown.apply(self.ctx,[errors]); });
//give a report
self._report(name, errors, totalTime);
-
- coverage(function(cd){
- self._coverqueue.forEach(function(lib){
- var file = fs.readFileSync(lib).toString().split("\n");
- var stats = cd[lib].stats();
- var c = "magenta", err = "red", g = "green";
-
- self._out("\n");
- self._out(c, "Module: "+path.basename(lib)+"\n");
- self._out(c, "Percentage seen: "+Math.floor(stats.percentage*100)+"%\n");
- self._out(c, "Lines seen: "+stats.seen+"\n");
- self._out(c, "Lines missed: "+stats.missing+"\n");
-
- stats.lines.forEach(function(line){
- self._out("\n");
- self._out(g, line.lineno-1+": "+file[line.lineno-2]+"\n");
- self._out(err, line.lineno+": "+line.source()+"\n");
- self._out(g, line.lineno+1+": "+file[line.lineno]+"\n");
- });
-
- self._out("\n");
- });
- coverage.release();
- });
+
+ timeoutsComplete++;
+ if(timeoutsComplete === self._queue.length){
+ self._reportCoverage();
+ if(callback) callback();
+ }
+
}, (opts.timeout > 0 ? opts.timeout : 250));
});
+
+}
+
+/*
+ * Runs only the coverage
+ *
+ * All output is written to stdout.
+ *
+ * This may be called many times,
+ * without an consequences.
+ *
+ * @api public
+ */
+
+stest.prototype.runCoverage = function(){
+ this._reportCoverage();
+
+ return this;
}
// Exports
View
4 test/test-coverage.js
@@ -1,6 +1,6 @@
var assert = require("assert"),
- stest = require("../lib/stest.js"),
- cov = stest.cover("./fixtures/coverage.js");
+ stest = require("../lib/stest"),
+ cov = stest.cover("./fixtures/coverage");
// defaults to 250 ms
var opts = { timeout: 0 };
View
25 test/test-stest.js
@@ -1,5 +1,13 @@
var assert = require("assert"),
- stest = require("../lib/stest");
+ fs = require("fs"),
+ util = require("util");
+
+// create a copy of stest
+fs.writeFileSync("../lib/stest2.js", fs.readFileSync("../lib/stest.js"));
+
+// use stest to test stest, easy as pie
+var ditto = require("../lib/stest2"),
+ stest = ditto.cover("../lib/stest");
// defaults to 250 ms
var opts = { timeout: 0 };
@@ -12,7 +20,7 @@ stest
promise.emit("ctx_2", this);
promise.emit("data", "hello");
},
- ctx_1: function(){
+ ctx_1: function(){
assert.ok(this.one);
assert.deepEqual("one", this.one);
},
@@ -31,14 +39,21 @@ stest
setup: function(promise){
setTimeout(function(){
promise.emit("ok");
- }, 1000);
+ }, 1000);
},
ok: function(){},
teardown: function(errors){
assert.ok(errors);
var err = errors.pop();
assert.deepEqual("ok", err.event);
assert.deepEqual("stest", err.type);
- }
+ }
})
-.run();
+.run(function(){
+ // Run only the coverage data, because
+ // you can't have the same file test itself
+ ditto.runCoverage();
+});
+
+// Cleanup
+fs.unlinkSync("../lib/stest2.js");

0 comments on commit ec05a3f

Please sign in to comment.
Something went wrong with that request. Please try again.