Wednesday, April 4, 2012

Timetravelling in JavaScript unit tests


At the February berlinjs meet-up Krzysztof Szafranek had a very nice, short talk about TDD in JavaScript (slides here). Among many other things he was showing busterjs and the way it lets you test asynchronous stuff without having to actually wait for time to pass.

I made a note of it to give it a closer look. When I recently had to write some JavaScript again, I remembered about it and was keen to give it a try. Since we were already using jasmine as our testing framework, my colleague Christoph and I were looking for something equivalent there. To cut the intro short, there is: jasmine.Clock.



The classic jasmine async helper methods

So let's look at an example. Suppose we want to test whether a focus event triggers the display of a tooltip. Since this behaviour happens asynchronously, you'd usually use jasmines runs() and waitsFor() functions to defer verifying your expectations until the code actually had a chance to run.
This has a couple of annoying limitations. First off, writing the test gets a bit more complicated and the test becomes harder to read. Next, if we’re only verifying such a simple behaviour (as a good unit test should) we end up duplicating the check in waitsFor() and in the actual expect() block. Admittedly, we could just leave out the last block, but that hurts the expressiveness of the test.

And then there’s the issue of time. Waiting for 200 ms doesn't seem that bad, but from my experience these things quickly add up. And what if we’re testing for something that’s happening with a larger delay? Making it even worse is the negative case. How long does it take until waitsFor() fails, if what we’re testing isn’t working (yet)? A timeout can be specified as another parameter to waitsFor but by default this is set at 5 seconds. And in the Unittestiverse, that is an eternity.

Using jasmine.Clock

So, here’s how we can test the same thing using jasmine.Clock:
This already reads a lot better in terms of describing the behaviour we’re interested in. It does completely hide the fact that the underlying code gets called asynchronously but I see that as a benefit in this case.

The other big benefit is that this test now runs in about 10 ms in my browser. No matter whether it fails or not.

I also like how this works internally. It's a simple idea, yet clever. And while clever things usually only end up causing trouble I don't mind taking that risk if it gives me such big benefits.

The setup call (useMock()) redefines JavaScripts core async methods setTimeout() and setInterval() (and clearTimeout() and clearInterval()) and replaces them with their own implementation, which stores the callback and the time after which to call it.

When you call Clock.tick(200) it executes the stored callbacks that were supposed to happen in that period, in the appropriate order. This happens synchronously from inside our test. So by the time we verify the expected behaviour, everything is hopefully in place.

After our test has run, jasmine restores the original functions and any other tests that might rely on the original behaviour should work fine.

Concluding

I like it a lot and am wondering why I only stumbled upon it now. If I don't care about the aync-ness of some behaviour - which is the default case, as it is just an implementation detail of the language - I can create more expressive tests with less effort.

I can probably come up with some cases where this way of testing might not be the best way to go. But my gut feeling is that those cases would smell of not being at the unit level.

A cursory glance at google suggests that there also might be some issues with specific features of other JavaScript libraries.

But tell me about your experience. Have you used this feature before? Did you run into problems? I have only just started writing the first tests in this way and maybe there are some caveats I should know?

No comments:

Post a Comment