Groke - JavaScript server for the JavaScript client
Groke is a research project to implement a platform for loading JavaScript applications into the browser. Applications may use whichever functionalities provided by the server (database, file i/o, networking, ...) and then the Groke platform dynamically replaces those invocations with RPC stubs upon application loading. So, when an application is loaded, groke inspects and instruments the code and sends it to the browser and whenever a "server side" function is called in the browser, an Ajax request is made in the background. This way application developers may implement their web applications in a more natural an concise way and not worry about the networking details. They can just use modules and services provided by the server as if they were local.
One of the key concepts in Groke is to use
server side
JavaScript to implement the whole platform. This means that as a
side product I have implemented features such as package /
namespace mechanism (via exports / require), database layer (ORM
like) and a test framework with
mocking. In the code I have followed the principles discussed and
agreed in the ServerJS
group. Source code is not available yet due to too
many todo comments in the code, but this will change
once I get more features implemented.
+-------------------------+ +--------------------+ | +---------------------+ | | application | | | application | | +--------------------+ | +---------------------+ | +--------------------+ | +---------------------+ | | groke | | | groke | | +--------------------+ | +---------------------+ | +--------------------+ | +---------------------+ | | browser | | | web server | | +--------------------+ | +---------------------+ | ^ | ^ | | | | Rhino | | +------+------------------+ | | +----------------------------+ HTTP / Ajax / Comet
Groke was first created to be used in conjunction with the Sun Labs Lively Kernel and the Lively (a research group that i'm part of) in general. However, there's nothing specific to the Lively environment in Groke. It can host any JavaScript applications.
Testing module
groke.test is a server side unit testing framework.
The syntax and some of the ideas follow the ones presented by
the
jsTest tool. So, big gratitude for that work. Features of the groke.test:
- JUnit style assert functions (assertTrue, assertEquals, assertFails, ...)
- setting up and tearing down tests
- easy mocking
- test functions are picked up by the test tool by exporting (using groke's export / require mechanism)
Sample test script (code comments are the documentation) :
// ** the top level code is run before each test function
// this test script needs these modules
var user = groke.require('domain').user;
var http = groke.require('http');
// we want to test this module
var handle = groke.require('controller').handle;
// create some initial data
db.connect();
var testuser = user();
testuser.username = 'jedi';
testuser.password = 'jedi';
user.save();
// ** test functions are specified by exporting them
exports.testRemoteDb = function () {
var requestMock, responseMock;
// ** create mocks with test.mock()
// at this stage groke.test finds all functions and
// creates stubs for them (http.request and http.response
// are groke wrappers for HttpServlet counterparts. they
// are not stored by the mock, they are only used for reflection)
requestMock = test.mock(http.request(null));
responseMock = test.mock(http.response(null));
// ** define expectations using the expect property
requestMock.expect.setHandled().times(1).withParameters(true);
requestMock.expect.getPathInfo().times(1).returns('/module/db/user');
requestMock.expect.getContent().times(1).returns(
'{"parameters":[{"type":"user","properties":["password"],'+
'"constraint":{"username":"jedi"}}]}');
responseMock.expect.setHeaders().times(1).withParameters(
{status: 200, contentType: 'application/jsonrequest'});
responseMock.expect.setContent().times(1).withParameters(
'{"data":"jedi"}');
handle(requestMock, responseMock);
// ** when the test finishes here, groke.test verifies all mocks
// to see that all expectations were met
};
// ** if tearDown is specified, it is run after each test function
exports.tearDown = function () {
try {
// drop user table
db.remove('user');
} catch (e1) {
}
// disconnect
try {
db.disconnect();
} catch (e2) {
}
};
"Use the source" (test.js):
/*
* 16.4.2009 jedi
*
* unit tester with mocking
*
* todo:
* - now we just throw Errors, use different kinds of exceptions
* - error messages are lousy
* - output formatting isn't the nicest..
* - differentiate errors and failures
* - integrate jslint
* - check that the module under test doesn't clutter global namespace
*
*
* mock is created by saying:
*
* var mymock = test.mymock(objctToBeMocked);
*
* if 'true' is given as optional parameter, mocking is done recursively.
* after that, the expectations for the mock are defined by saying
* something like this:
*
* mymock.expect.myfunction().times(1).withParameters('hello').returns(true);
*
* mock.expect has following functions:
*
* - times() // 0 means that it can't be invoked
* - withParameters()
* - returns()
*
* functions that are exported in the test file are run individually. so,
* the whole test file is reloaded after each test functions. this means
* that all setup code can lie in the top level of the test script. if a
* teardown() function is exported it is invoked after each test function
*
*/
/*jslint onevar: true, undef: true, eqeqeq: true, rhino: true,
plusplus: true, bitwise: true, newcap: true, forin: true */
/*global groke exports */
// load javascript 'language extensions'
load('src/core.js');
load('lib/json2.js');
// todo: where do we get paths to these files?
/**
* test namespace has functions for mocking and asserting
*/
var test = function () {
var objectStub, expectationFunctionStub, functionStub, util,
log, expectations = [];
util = groke.require('util');
log = groke.require('logging').log;
/**
* @param name name of the function
* @param expectation the expectation object
* @return 'normal' stub function
*/
functionStub = function (name, expectation) {
return function () {
expectation.__funcCalled(util.argumentsToArray(arguments));
return expectation.__returnValue();
};
};
/**
* @param name name of the function
* @return 'expectation' stub function
*/
expectationFunctionStub = function (name) {
var expFuncStub, times_ = 0, numberOfCalls = 0, returns_ = [],
parameters_ = [];
// todo: figure out how to make __xxx functions protected.
// i.e. private for the test application but visible for the mock
expFuncStub = {
/// mock object calls this
__verify: function () {
if (times_ !== -1 && (times_ - numberOfCalls !== 0)) {
throw new Error(name + ' called ' + numberOfCalls +
' times when it should have been called ' +
times_ + ' times');
}
},
__funcCalled: function (args) {
var i, expectedArgs = parameters_.shift();
numberOfCalls += 1;
// check that the function hasn't been called too many times
if (times_ !== -1 && (times_ - numberOfCalls < 0)) {
throw new Error(name + ' called ' + numberOfCalls +
' times when only ' + times_ +
' allowed\n');
}
// if parameters have been defined, check that they are what
// was expected
if (expectedArgs !== undefined &&
!util.isEqual(expectedArgs, args)) {
throw new Error(
name + ' called with wrong parameters (' +
args.toSource() + ' !== ' +
expectedArgs.toSource() + ')');
}
},
__returnValue: function () {
return returns_.shift();
},
times: function (n) {
times_ = n;
return this;
},
returns: function (obj) {
returns_.push(obj);
return this;
},
withParameters: function () {
parameters_.push(util.argumentsToArray(arguments));
return this;
}
};
return function () {
return expFuncStub;
};
};
/**
* @param obj object to be mocked
* @param recurse optional parameter. if you set it to true,
* mocking is done recursively until all child objects have been
* mocked too
* @return a mock object generated based on the given object
*/
objectStub = function (obj, recurse) {
var o, x, that = {};
that.expect = {};
for (o in obj) {
try {
// i know, this is amazing, but it seems that when
// mocking java objects you run into situations where
// a property is part of an object, yet it is not
// accessible
// todo: find out more about this
x = obj[o];
} catch(e) {
log.warning('couldn\'t mock property \'' + o + '\'');
continue;
}
if (o === 'expect') {
throw new Error('given object isn\'t mockable because it ' +
'already have a property called \'expect\'');
} else if (typeof obj[o] === 'function') {
// create stub functions for the mock an for the expectation
that.expect[o] = expectationFunctionStub(o);
that[o] = functionStub(o, that.expect[o]());
expectations.push(that.expect[o]());
} else if (typeof obj[o] === 'object') {
if (recurse) {
// call 'objectStub()' recursively on objects
that[o] = objectStub(obj[o], true);
}
} else {
// just copy primitive types (no need to put these into
// expectation
that[o] = obj[o];
}
}
return that;
};
return {
/// @return a mock object generated from the given object
mock: function (obj, recurse) {
var that = {};
that = objectStub(obj, recurse);
return that;
},
releaseMocks: function () {
var i;
for (i = 0; i < expectations.length; i += 1) {
expectations[i].__verify();
}
expectations = [];
},
testException: function (msg) {
var that = {
message: msg
};
return that;
},
fail: function (msg) {
// todo: add debug info (file and line number)
//throw this.testException(msg);
throw new Error(msg);
},
assertTrue: function (property) {
if (property !== true) {
this.fail('assertTrue: ' + property);
}
},
assertFalse: function (property) {
if (property !== false) {
this.fail('assertFalse: ' + property);
}
},
assertUndefined: function (property) {
if (typeof property !== 'undefined') {
this.fail('assertUndefined: ' + property);
}
},
assertEquals: function (a, b) {
if (a !== b) {
this.fail('assertEquals: \'' + a + '\' != \'' + b + '\'');
}
},
assertEqualsAny: function () {
var i, tested = arguments[0];
for (i = 1; i < arguments.length; i += 1) {
if (tested === arguments[i]) {
return;
}
}
this.fail('assertEqualsAny: ' + arguments[0] +
' didn\'t match any of the arguments');
},
assertType: function (type, obj) {
if (typeof obj !== type) {
this.fail('assertType: ' + obj + ' is not of type ' + type);
}
},
assertMatch: function (str, match) {
if (str.match(match) === null) {
this.fail('assertMatch: \'' + match + '\' not found in ' + str);
}
},
assertError: function () {
var i, args = [], func = arguments[0], error = false;
try {
for (i = 1; i < arguments.length; i += 1) {
args.push(arguments[i]);
}
func.apply(null, args);
} catch (e) {
error = true;
}
if (!error) {
this.fail(func + ' was supposed to throw');
}
}
};
}();
// create an anonymous function and invoke it with command line
// argument list. tests are loaded using groke's normal require /
// exports mechanism
(function (files) {
var i, log, run, printResults, printResult, blanks, results, conf,
tearDown, tearDownFunc = 'tearDown';
// load the conf
conf = groke.require('conf');
// todo: where does the path come from?
conf.load('test/groke.conf');
log = groke.require('logging').log;
blanks = ' ';
results = {
success: 0,
total: 0,
failed: 0,
errors: 0
};
printResult = function (title, value) {
print(title + blanks.slice(title.length - blanks.length) + value);
};
printResults = function () {
var res;
print('-- test results -------------------------------------------');
for (res in results) {
if (results.hasOwnProperty(res)) {
printResult(res + ':', results[res]);
}
}
};
tearDown = function (tester) {
try {
if (tester.hasOwnProperty(tearDownFunc) &&
typeof tester[tearDownFunc] === 'function') {
tester[tearDownFunc]();
}
} catch (e) {
log.error('failed to tear down test: ' + e);
}
};
// run all tests specified in the given file
run = function (file) {
var tester, o, i, funcs = [];
// gather the names of all test functions into 'funcs'
// (this is done by loading and unloading the script)
tester = groke.require(file, true);
for (o in tester) {
if (tester.hasOwnProperty(o)) {
if (typeof tester[o] === 'function' && o !== tearDownFunc) {
funcs.push(o);
}
}
}
tearDown(tester);
// invoke all test function on at a time and reload the
// test script for each test case
for (i = 0; i < funcs.length; i += 1) {
// reload test script
try {
tester = groke.require(file, true);
results.total += 1;
// run the test function
tester[funcs[i]]();
// release mocks
test.releaseMocks();
results.success += 1;
} catch (e) {
// todo: separate errors and test failures
results.failed += 1;
log.error(file + ':' + funcs[i] + '():');
log.error('file: ' + e.fileName + ' line: ' +
e.lineNumber + ': ' + e.message);
if (e.rhinoException) {
e.rhinoException.printStackTrace();
}
}
finally {
tearDown(tester);
}
}
};
// test all given files
for (i = 0; i < files.length; i += 1) {
run(files[i]);
}
printResults();
})(arguments);