Permalink
Browse files

switch testing frameworks

  • Loading branch information...
joshbuddy committed May 10, 2010
1 parent c51fe8b commit 55893c6709f4e3715bba6fa62f0bb75391aac0fc
View
@@ -0,0 +1,100 @@
+# About
+
+Simple test framework for asynchronous testing in [Node.js](http://nodejs.org/). It's trying to be as simple and explicit as possible. No magic, no wheel reinventing. Just use minitest for building your tests and the [assert library](http://nodejs.org/api.html#assert-212) for the actual helpers for testing equality etc.
+
+This is how the output looks like:
+
+![Minitest.js output](http://github.com/botanicus/minitest.js/raw/master/minitest.png)
+
+# Setup
+
+* `require()` minitest
+* Use `minitest.setupListeners()` for listening on the `uncaughtException` and `exit` events.
+* Use `minitest.context(description, block)` for defining your contexts. Context will be usually a function or object name.
+* Use `#<a Context>.assertion(description, block)` for defining your assertions.
+* Use `#<a Test>.finished()` to mark test as finished. All the tests has to have it. Without this you won't be able to write solid asynchronous tests, because you can't ask only "is a and b the same?", but also "did the callback run?".
+* Run `node foo_test.js` to get the results.
+
+# Example
+
+ var minitest = require("minitest");
+ var assert = require("assert");
+
+ minitest.setupListeners();
+
+ minitest.context("Context#setup()", function () {
+ this.setup(function () {
+ this.user = {name: "Jakub"};
+ });
+
+ this.assertion("it should setup listeners", function (test) {
+ // test something via the standard assert module
+ assert.ok(this.user)
+
+ // mark test as finished
+ test.finished();
+ });
+
+ this.assertion("it should be able to count", function (test) {
+ if (2 !== 4) {
+ // manually fail the test
+ throw new Error("You can't count, can you?");
+ };
+ });
+ });
+
+## Formatters
+
+If you don't like minitest output, you can simply override following methods:
+
+* `Context.prototype.contextHeader()`
+* `Test.prototype.reportSuccess()`
+* `Test.prototype.reportError(error)`
+* `Test.prototype.reportNotRun()`
+
+All this methods are supposed to return a string and all these methods have access to `this.description`.
+
+# Common Problems in Testing Asynchronous Code
+
+## Exceptions in Callbacks
+
+Obviously you can't catch errors which occured in callbacks. Consider following:
+
+ try {
+ db.get("botanicus", function (user) {
+ throw new Error("You can't catch me!");
+ });
+ } catch(error) {
+ // you'll never get in here
+ };
+
+## Testing Exceptions
+
+ this.assertion("should throw an error", function (test) {
+ assert.throws(function () {
+ throw new Error("Error occured!");
+ test.finished();
+ });
+ });
+
+This obviously can't work, because exception interrupts the anonymous function we are passing as an argument for `assert.throws()`.
+
+ this.assertion("should throw an error", function (test) {
+ assert.throws(function () {
+ throw new Error("Error occured!");
+ });
+ test.finished();
+ });
+
+This is better, it will at least work, but what if there will be an error in the `assert.throws()` function and it doesn't call the anonymous function?
+
+ this.assertion("should throw an error", function (test) {
+ assert.throws(function () {
+ test.finished();
+ throw new Error("Error occured!");
+ });
+ });
+
+OK, this is better, `test.finished()` doesn't jump out of the test, so in case that the assertion will fail, we will get the proper result. However it's not perfect, because I can change `test.finished()` in future to actually jump out of the function (I probably won't do that but you can't know) plus if there would be a bug, so `test.finished()` would cause an exception, it would satisfy `assert.throws()` without actually testing the code. Well, you'd probably noticed in other tests, but still.
+
+Fortunatelly you can specify error class and expected message for `assert.throws()` in this order: `assert.throws(block, error, message)`.
@@ -0,0 +1,9 @@
+- Fix the unicode issues with Node.js 0.1.90. It does work with Node.js 0.1.33, but in 0.1.90 it just prints first two lines (context description and first successful message) and then don't output anything (but the code is still running and we are even able to inspect these messages which should be outputted via sys.p()).
+
+Fix this:
+context("foo", function () {
+ var test = true;
+ this.assertion("bar", function (test) {
+ sys.p(test); // undefined, which is because of the magic with call
+ });
+});
@@ -0,0 +1,35 @@
+/*
+ Simple module for coloured output for POSIX shells.
+
+ @example
+ colours.bold.green + "OK: " + colours.reset + description
+*/
+
+colours = {
+ reset: "\x1B[0m",
+
+ grey: "\x1B[0;30m",
+ red: "\x1B[0;31m",
+ green: "\x1B[0;32m",
+ yellow: "\x1B[0;33m",
+ blue: "\x1B[0;34m",
+ magenta: "\x1B[0;35m",
+ cyan: "\x1B[0;36m",
+ white: "\x1B[0;37m",
+
+ bold: {
+ grey: "\x1B[1;30m",
+ red: "\x1B[1;31m",
+ green: "\x1B[1;32m",
+ yellow: "\x1B[1;33m",
+ blue: "\x1B[1;34m",
+ magenta: "\x1B[1;35m",
+ cyan: "\x1B[1;36m",
+ white: "\x1B[1;37m",
+ }
+};
+
+// exports
+for (colour in colours) {
+ exports[colour] = colours[colour];
+};
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+
+var assert = require("assert");
+var minitest = require("./minitest");
+
+// setup listeners
+minitest.setupListeners();
+
+// tests
+minitest.context("Minitest.js", function () {
+ this.assertion("it should succeed", function (test) {
+ assert.ok(true);
+ test.finished();
+ });
+
+ this.assertion("it should fail", function (test) {
+ assert.ok(null);
+ test.finished();
+ });
+
+ this.assertion("it should not be finished", function (test) {
+ assert.ok(true);
+ });
+});
@@ -0,0 +1,158 @@
+var sys = require("sys");
+var colours = require("./colours");
+
+/* suite */
+function Suite () {
+ this.contexts = [];
+};
+
+Suite.prototype.report = function () {
+ var suite = this;
+ this.contexts.forEach(function(context, index) {
+ sys.puts(context.contextHeader());
+ context.report();
+ if (suite.contexts.length === index) {
+ sys.puts("");
+ };
+ });
+};
+
+Suite.prototype.register = function (context) {
+ this.contexts.push(context);
+};
+
+// there is only one suite instance
+var suite = exports.suite = new Suite();
+
+/* context */
+function Context (description, block) {
+ this.tests = [];
+ this.block = block;
+ this.description = description;
+};
+
+Context.prototype.run = function () {
+ this.block.call(this);
+};
+
+Context.prototype.register = function (test) {
+ this.tests.push(test);
+};
+
+Context.prototype.report = function () {
+ this.tests.forEach(function (test) {
+ test.report();
+ });
+};
+
+/* test */
+function Test (description, block, setupBlock) {
+ this.description = description;
+ this.block = block;
+ this.setupBlock = setupBlock;
+};
+
+Test.prototype.run = function () {
+ try {
+ if (this.setupBlock) {
+ this.setupBlock.call(this);
+ };
+
+ this.block.call(this, this);
+ } catch(error) {
+ this.failed(error);
+ };
+};
+
+Test.prototype.finished = function () {
+ this.result = this.reportSuccess();
+};
+
+Test.prototype.failed = function (error) {
+ this.result = this.reportError(error);
+};
+
+Test.prototype.report = function () {
+ if (this.result) {
+ sys.puts(this.result);
+ } else {
+ sys.puts(this.reportNotFinished());
+ };
+};
+
+/* output formatters */
+Context.prototype.contextHeader = function () {
+ return colours.bold.yellow + "[= " + this.description + " =]" + colours.reset;
+};
+
+Test.prototype.reportSuccess = function () {
+ // return colours.bold.green + " ✔ OK: " + colours.reset + this.description;
+ return colours.bold.green + " OK: " + colours.reset + this.description;
+};
+
+Test.prototype.reportError = function (error) {
+ var stack = error.stack.replace(/^/, " ");
+ // return colours.bold.red + " ✖ Error: " + colours.reset + this.description + "\n" + stack;
+ return colours.bold.red + " Error: " + colours.reset + this.description + "\n" + stack;
+};
+
+Test.prototype.reportNotFinished = function () {
+ // return colours.bold.magenta + " ✖ Didn't finished: " + colours.reset + this.description;
+ return colours.bold.magenta + " Didn't finished: " + colours.reset + this.description;
+};
+
+/* DSL */
+function context (description, block) {
+ var context = new Context(description, block);
+ suite.register(context);
+ context.run();
+};
+
+/*
+ Run an example and print if it was successful or not.
+
+ @example
+ minitest.context("setup()", function () {
+ this.assertion("Default value should be 0", function (test) {
+ assert.equal(value, 0);
+ test.finished();
+ });
+ });
+*/
+Context.prototype.assertion = function (description, block) {
+ var test = new Test(description, block, this.setupBlock);
+ this.register(test);
+ test.run();
+};
+
+Context.prototype.setup = function (block) {
+ this.setupBlock = block;
+};
+
+function runAtExit () {
+ process.addListener("exit", function () {
+ suite.report();
+ });
+};
+
+function setupUncaughtExceptionListener () {
+ // TODO: is there any way how to get the test instance,
+ // so we could just set test.result, so everything would be
+ // reported properly on the correct place, not in the middle of tests
+ process.addListener("uncaughtException", function (error) {
+ sys.puts(Test.prototype.reportError(error));
+ });
+};
+
+function setupListeners () {
+ setupUncaughtExceptionListener();
+ runAtExit();
+};
+
+/* exports */
+exports.Context = Context;
+exports.Test = Test;
+exports.context = context;
+exports.runAtExit = runAtExit;
+exports.setupUncaughtExceptionListener = setupUncaughtExceptionListener;
+exports.setupListeners = setupListeners;
Binary file not shown.
@@ -0,0 +1,31 @@
+#!/usr/bin/env node
+
+var assert = require("assert");
+var minitest = require("./minitest");
+
+// setup listeners
+minitest.setupListeners();
+
+// tests
+minitest.context("Namespacing", function () {
+ this.setup(function () {
+ this.user = {name: "Jakub"};
+ });
+
+ this.assertion("instance variable from setup() should exist in assertion", function () {
+ assert.ok(this.user);
+ this.finished();
+ });
+
+ var foobar = true;
+ this.assertion("local variable from context of the current context should exist in assertion", function () {
+ assert.ok(foobar);
+ this.finished();
+ });
+
+ this.foobaz = true;
+ this.assertion("instance variable from context of the current context should not exist in assertion", function () {
+ assert.equal(undefined, this.foobaz);
+ this.finished();
+ });
+});
View
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+node spec/spec_generate.js
+node spec/spec_recognize.js
Oops, something went wrong.

0 comments on commit 55893c6

Please sign in to comment.