JS.Class 3.0.8: source maps, prototype stubs, and async error catching

I don’t usually blog point releases, but JS.Class releases tend to be infrequent these days, and mostly polish what’s there rather than significantly changing things. This release is no different, but the few changes it contains make it significantly more usable.

First, it now (like everything I ship for the browser) comes with source maps. Thanks to Jake, this was a simple configuration change.

Second, it fixes a bug in the stubbing library that means instance methods on prototypes can now be stubbed. For example, I was recently writing some new tests for Songkick’s Spotify app, which we run these tests in Chrome. (Being based on WebKit, the Spotify platform is close enough that you can write useful unit tests and run them in Chrome or v8/Node.) Spotify adds some methods to built-in prototypes though, and our code relies on them, so they need to be present while running tests in Chrome. I could just implement them globally, but there are other use cases where you just need to stub a method on all instances of a class during one test. So, this now works, and the stub is removed (and verified if it’s a mock) at the end of the test:

stub(String.prototype, "decodeForText", function() { return this.valueOf() })
"foo".decodeForText() // -> "foo"

Finally, I’ve fixed a major issue that’s been bugging me with JS.Test. As I’ve done more projects on Node.js, I’ve found that it’s way too easy to crash the test run completely because an error was thrown in a section of async code. Because it’s outside the test framework’s call stack, it doesn’t get caught, and Node just bails out:

$ npm test

> restore@0.1.0 test /home/james/projects/restore
> node spec/runner.js

Loaded suite WebFinger, OAuth, Storage, Stores, File store

Started
..
/home/james/projects/restore/node_modules/jsclass/src/test.js:1899
          throw new JS.Test.Mocking.UnexpectedCallError(message);
                ^
Error: <store> received call to authorize() with unexpected arguments:
( "the_client_id", "zebcoe", { "the_scope": [ "r", "w" ] }, #function )
npm ERR! Test failed.  See above for more details.
npm ERR! not ok code 0

This happens most often because I have a test that uses mocks, for example when I send a certain request to a server, I expect the server to tell the underlying model to do something.

it("authorizes the client", function() { with(this) {
  expect(store, "authorize").given("the_client_id", "zebcoe", {the_scope: ["r", "w"]}).yielding([null, "a_token"])
  http_post("/auth", auth_params)
}})

When I change the mock expectation this makes previously working code call a method with unexpected arguments, which throws an error, and because the HTTP request is processed asynchronously, the error is not caught. But it also happens for all sorts of other reasons, for example you have code that calls fs.readFile(), then processes the contents before calling you back – if the pre-processing fails, the error crashes the process.

Well now this error gets caught, so you get useful feedback from your tests when these types of errors happen:

$ npm test

> restore@0.1.0 test /home/james/projects/restore
> node spec/runner.js

Loaded suite WebFinger, OAuth, Storage, Stores, File store

Started
..E...............................................

1) Error:
OAuth with valid login credentials authorizes the client:
Error: <store> received call to authorize() with unexpected arguments:
( "the_client_id", "zebcoe", { "the_scope": [ "r", "w" ] }, #function )

Finished in 0.851 seconds
50 tests, 111 assertions, 0 failures, 1 errors

npm ERR! Test failed.  See above for more details.
npm ERR! not ok code 0

Now the error is caught, the tests all finish, and you get a clear report about which test caused the error.

This functionality is supported on Node.js and in the browser. As far as I know (and I’ve tried a lot of different frameworks) the only other test frameworks that do this are Mocha and Buster. If you have a similar problem, you can catch uncaught errors like this:

// Node.js
process.addListener('uncaughtException', function(error) {
  // handle error
});

// Browsers
window.addEventListener('error', function(event) {
  // handle event
}, false);

On Node, this is particularly useful for stopping servers crashing in case of an error. In the browser, it’s mostly useful for reporting, only because the argument to the callback is a DOM event rather than an exception object, the information you can get out of it tends to be lacking. Note that for old IEs you’ll need to use window.attachEvent('onerror'), and Opera only supports catching these errors with window.onerror.

While researching this, I was really surprised to see how many very widely used frameworks don’t do this. The best alternative I’ve seen is in Jasmine: this example does not report the async error, but the test times out because it is never resumed. jasmine-node doesn’t catch this at all, which is why the test is only run if global.window exists. I’ve seen several other frameworks that either crash on Node, or in the browser simply stop updating the view, giving you no feedback that the test runner has halted without running all the tests.

Since most frameworks don’t catch these errors, I would assume this isn’t actually a problem for most people. Is this true? I’d like to know how other people deal with this situation.

If you want to give JS.Class a go, just run npm install jsclass or download it from the website.

If you’ve enjoyed this article, you might enjoy my recently published book JavaScript Testing Recipes. It’s full of simple techniques for writing modular, maintainable JavaScript apps in the browser and on the server.