www.kuuskeri.com

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:

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);



webmaster(gmail)