biril / backbone-faux-server

A framework for mocking up server-side persistence / processing for Backbone.js
MIT License
54 stars 11 forks source link
backbone backbonejs mock persistence test

Backbone Faux Server

Build Status NPM version Bower version

A framework for mocking up server-side persistence / processing for Backbone.js

Define any number of routes that map <model-URL, sync-method> pairs to custom handlers. Faux-server overrides (is a drop-in replacement of) Backbone's native sync so that whenever a Model (or Collection) is synced and its URL along with the sync method form a pair that matches a defined route, the route's handler is invoked. Implement handlers to test the expected behaviour of your app, work with dummy data, support persistence using local-storage, etc. When & if you choose to move to a real server, switching back to Backbone's native, ajax-based sync is as simple as fauxServer.enable(false).

Backbone faux server (henceforth 'BFS') grew out of the author's need to quickly flesh out Backbone prototype apps without having to fiddle with a server, a DB, or anything else that would require more than a JS script. Similar solutions exist for this but they deviate from or obscure Backbone's opinion of Model URLs, REST and their interdependence. Additionally, BFS doesn't implement some specific persistence scheme but only provides hooks for your own custom processing / persistence scheme, per HTTP verb, per resource (Model or Collection URL). Functionality written this way, may be ported to the server-side in a straightforward manner.

Set up

To get Backbone Faux Server

BFS may be used as an exported global, a CommonJS module or an AMD module depending on the current environment:

Usage

Define Backbone Models and Collections as you normally would:

var Book = Backbone.Model.extend({
    defaults: {
        title: "Unknown title",
        author: "Unknown author"
    }
});
var Books = Backbone.Collection.extend({
    model: Book,
    url: "library-app/books"
});

Note that the url property is used, as it would in any scenario involving a remote resource.

Continue by defining routes, to handle Model syncing as needed. Every route defines a mapping from a Model(or Collection)-URL & sync-method (an HTTP verb (POST, GET, PUT, PATCH, DELETE)) to some specific handler (callback):

<model-URL, sync-method> → handler

For example, to handle the creation of a Book (Books.create(..)), define a route that maps the <"library-app/books", "POST"> pair to a handler, like so:

fauxServer.addRoute("createBook", "library-app/books", "POST", function (context) {
    // Every handler receives a 'context' parameter. Use context.data (a hash of
    //  Book attributes) to create the Book entry in your persistence layer.
    //  Return attributes of created Book. Something along the lines of:
    context.data.id = newId(); // You'll probably want to assign an id to the new book
    books.push(context.data);  // Save to persistence layer
    return context.data;
});

The "createBook" parameter simply defines a name for the route. The URL parameter, "library-app/books", is pretty straightforward in the preceding example - it's the URL of the Books Collection. Note however that the URL may (and usually will) be specified as a matching expression, similarly to Backbone routes: URL-expressions may contain parameter parts, :param, which match a single URL component between slashes; and splat parts *splat, which can match any number of URL components. Optional parts are denoted using parentheses. The values captured by params and splats will be passed as extra parameters to the given handler method. Regular expressions may also be used, in which case all values captured by reg-exp capturing groups will be passed as extra parameters to the handler method.

Define more routes to handle updating, reading and deleting Models. The addRoutes method is used below to define routes to handle all actions (create, read, update and delete) for the preceding Book example:

fauxServer.addRoutes({
    createBook: {
        urlExp: "library-app/books",
        httpMethod: "POST",
        handler: function (context) {
            // Create book using attributes in context.data
            // Save to persistence layer
            // Return attributes of newly created book
        }
    },
    readBooks: {
        urlExp: "library-app/books",
        httpMethod: "GET",
        handler: function (context) {
            // Return array of stored book attributes
        }
    },
    readBook: {
        urlExp: "library-app/books/:id",
        httpMethod: "GET",
        handler: function (context, bookId) {
            // Return attributes of stored book with id 'bookId'
        }
    },
    updateBook: {
        urlExp: "library-app/books/:id",
        httpMethod: "PUT",
        handler: function (context, bookId) {
            // Update stored book with id 'bookId', using attributes in context.data
            // Return updated attributes
        }
    },
    deleteBook: {
        urlExp: "library-app/books/:id",
        httpMethod: "DELETE",
        handler: function (context, bookId) {
            // Delete stored book of id 'bookId'
        }
    }
}

Route names can be useful for querying and / or removing earlier defined routes. However, this is often unnecessary and route names may be skipped in most declarations. (They're mandatory as keys when passing a hash of routes to addRoutes.) Coming back to the earlier "createBook" example, the route name may be skipped like so:

fauxServer.addRoute("library-app/books", "POST", function (context) {
    // Create book ..
});

Moreover, faux-server exposes get, post, put, del and patch methods as shortcuts for calling addRoute with a specific httpMethod. Thus, the preceding POST-route addition may be rewritten as

fauxServer.post("library-app/books", function (context) {
    // Create book ..
});

Thus, an alternative, more compact syntax for the preceding addRoutes example would be:

fauxServer
    .post("library-app/books", function (context) {
        // Create book using attributes in context.data
        // Save to persistence layer
        // Return attributes of newly created book

    }).get("library-app/books", function (context) {
        // Return array of stored book attributes

    }).get("library-app/books/:id", function (context, bookId) {
        // Return attributes of stored book with id 'bookId'

    }).put("library-app/books/:id", function (context, bookId) {
        // Update stored book with id 'bookId', using attributes in context.data
        // Return updated attributes

    }).del("library-app/books/:id", function (context, bookId) {
        // Delete stored book of id 'bookId'
    });
}

Testing / Contributing

The QUnit test suite may be run in a browser (test/index.html) or on the command line, by running make test or npm test. The command line version runs on Node and depends on node-qunit (npm install to fetch it before testing). A coverage report is also available.

Contributions are obviously appreciated. Please commit your changes on the dev branch - not master. dev is always ahead, contains the latest state of the project and is periodically merged back to master with the appropriate version bump. In lieu of a formal styleguide, take care to maintain the existing coding style. Please make sure your changes test out green prior to pull requests.

Reference

The following list, while not exhaustive, includes all essential parts of the BFS API. The omitted bits are there facilitate fancier stuff you probably won't ever need. Further insight may be gained by taking a look at the examples, the test suite and - of course - the source. For the latter, an annotated version is also maintained.

Methods

All methods return the faux-server instance and may be chained, unless otherwise noted.

addRoute ([name, ]urlExp[, httpMethod][, handler])

Add a route to the faux-server. Every route defines a mapping from a Model(or Collection)-URL & sync-method (an HTTP verb (POST, GET, PUT, PATCH or DELETE)) to some specific handler (callback):

<model-URL, sync-method> → handler

Whenever a Model is created, read, updated or deleted, its URL and the the sync method being used are tested against defined routes in order to find a handler for creating, reading, updating or deleting this Model. The same applies to reading Collections: Whenever a Collection is read, its URL (and the 'read' method) will be tested against defined routes in order to find a handler for reading it. When a match for the <model-URL, sync-method> pair is not found among defined routes, the native sync is invoked (this behaviour may be overridden - see fauxServer.setDefaultHandler). Later routes take precedence over earlier routes so in configurations where multiple routes match, the one most recently defined will be used.

<httpMethod> ([name, ]urlExp[, handler])

get, post, put, del and patch methods which act as shortcuts for calling addRoute with a specific httpMethod. See addRoute above for parameter descriptions and further details.

addRoutes (routes)

Add multiple routes to the faux-server.

removeRoute (name)

Remove the route of given name.

removeRoutes ()

Remove all defined routes.

getRoute (name)

Get route of given name.

setDefaultHandler ([handler])

Set a handler to be invoked when no route is matched to the current <model-URL, sync-method> pair. This will override the default behaviour of invoking the native sync.

setLatency (min[, max])

Set server's emulated latency (zero by default)

setTransportFactory (transportFactory)

Set server's transport factory

Transports are deferred-like objects implementing a resolve / reject / promise interface. A successful sync will invoke transport.resolve while a failed one will invoke transport.reject. The sync method will always return transport.promise().

See the Transport section for further details.

enable ([shouldEnable])

Enable or disable the faux-server. When disabled, syncing is performed by the native Backbone sync method. Handy for easily toggling between mock / real server.

getVersion ()

Get the faux-server version

noConflict ()

Run in no-conflict mode, setting the global fauxServer variable to to its previous value. Only useful when working in a browser environment without a module-loader as this is the only case where fauxServer is exposed globally. Returns a reference to the faux-server.

Transports

Backbone is built on minimum assumptions regarding the communication layer and/or persistence strategy applications will make use of. Although, more often that not, this is jQuery's ajax function, you may choose to use a modified ajax function or bypass the sync method altogether (this is in fact how BFS works). Backbone will, generally speaking, abstract this away so that applications may be written on top of a normalized layer.

Having said that, specific choices in the method of communication / persistence can affect how application code is written in certain ways. As an example consider the case of chaining a then call after sync:

aModel.save().then(function () {
    // .. continue after successfully saving the model ..
});

This works under the assumption that Backbone's sync returns a promise, which is the case when the underlying ajax function - the communication layer - does so. Which holds true specifically for jQuery's ajax but not necessarily for other communication layers and/or persistence strategies.

As BFS is itself such a strategy, one that completely bypasses the communication layer, there may be cases where application code assumes and makes use of functionality which will not be available when the app is run on a faux server. A common problematic example is implementations which rely on the creation of jqXHR objects when Models are synced: BFS sync will attempt to return a jQuery promise when that's feasible (when Backbone.$ is found to be the jQuery object at runtime) or just return undefined otherwise - never will it return an actual jqXHR. You can compensate for that by implementing a custom 'transport'.

Transports are a BFS abstraction intended as a means of mocking the aspects (the API) of an application-specific communication layer. It may be helpful to think of the jqXHR object as a concrete example of a transport - or, to be precise, a transport's promise.

Transports are deferred-like objects implementing a resolve / reject / promise interface. BFS will instantiate a new transport on every sync, and return its promise by invoking transport.promise. When the sync is successful, i.e. when the relevant handler returns a non-string value, transport.resolve will be called with the handler's returned result. When the sync fails, i.e. when the relevant handler returns a string result, transport.reject will be called with the handler's result. It is the transport's responsibility to subsequently call the given success or error callbacks.

To define a custom transport, to be instantiated on every invocation of sync, call fauxServer.setTransportFactory providing a transport-factory function. Implement your custom transport-factory function so that

As a reference, this is (a somewhat simplified version of) the default BFS transport-factory:

// Transport-factory function, invoked _per sync_ (with the relevant options / context)
//  to instantiate a new transport
function (syncOptions, syncContext) {
    // If an underlying ajax lib is defined for Backbone and it features a
    //  Deferred method (which is precisely the case when Backbone.$ = jQuery)
    //  then create and return a deferred object as transport
    if (Backbone.$ && Backbone.$.Deferred) {
        var deferred = Backbone.$.Deferred();
        deferred.then(syncOptions.success, syncOptions.error);
        return deferred;
    }

    // Otherwise create a poor-man's deferred - an object that implements a
    //  promise/resolve/reject interface without actual promise semantics:
    //  resolve and reject just delegate to success and error callbacks while
    //  promise() returns undefined. This is a good enough transport
    return {
        promise: function () {},
        resolve: function (value) { syncOptions.success(value); },
        reject: function (reason) { syncOptions.error(reason); }
    };
}

Caveats / WTF

License

Licensed and freely distributed under the MIT License (LICENSE.txt).

Copyright (c) 2012-2014 Alex Lambiris