Rubyist's first attempt at testing JavaScript

I have to confess something. I have written way too many lines of JavaScript without ever writing a single test. Untested JavaScript used to be silently accepted. All the JavaScript tutorials I did never even mentioned testing. Fortunately, it’s all changing rapidly. Testing is becoming a must in the JavaScript community as much as it is in all the others.

I have joined a React project recently. It was already setup with Mocha. I was very pleasantly surprised to find out how much it resembled RSpec. I want to share with you a side by side comparison of identical test suites, the first one written in RSpec, the second one in Mocha.

The test subjects

To make the examples easier to understand, let’s break the rules of TDD and write the code under test first.

A Bomb can be defused or detonated. When detonated, it raises an error. It can be detonated instantly or with a delay. It has a production date.

A RedButton detonates a bomb with a delay of 1000 ms.

Ruby

# lib/bomb.rbclassBombdefinitialize@production_date=Time.nowenddefdetonate(delay=nil)sleepdelayifdelayraise'You are dead. No more coding.'unless@defusedenddefcut_wire(color)ifthe_right_wire==color@defused=trueelsedetonateendenddefthe_right_wirerand<0.5?:blue::redenddefproduction_date@production_dateendend

JavaScript (ES6)

// lib/bomb.jsexportdefaultclassBomb{constructor(){this.productionDate=newDate;}detonate(delay){letgoOff=()=>{if(!this.disarmed){throw'You are dead. No more coding.';}};if(delay){setTimeout(goOff,delay);}else{goOff();}}cutWire(color){if(this.theRightWire()==color){returnthis.disarmed=true;}else{this.detonate();returnfalse;}}theRightWire(){returnMath.random()<0.5?'blue':'red'}getProductionDate(){returnthis.productionDate;}}

Proxyquire - a tool for messing with the way modules get imported, allows us to swap some dependencies of a module we want to test to mocks without changing anything in the module’s source code, works with ES6 modules and Babel,

Setup

RSpec

The default setup of RSpec done with rspec --init creates a setup file called spec_helper.rb, which is loaded with --require spec_helper in .rspec.

Mocha

Mocha has no convention of a setup file. There is a way to load a file running the test suite with mocha --require foo.js. In this file, however, Mocha is not available (we might want to have Mocha available to set global before and after hooks) . To solve this problem I have decided to create a test_helper.js and simply include it in every test file. Mocha loads all .js files from the test directory before running any tests, so that’s technically not necessary unless you want to run separate test files, which I definitely do.

Basic syntax

It’s practically identical. We have the same describe, context, it, before and after methods, with the exception of beforeEach and afterEach being separate methods.

RSpec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# spec/lib/bomb_spec.rbrequire_relative'../../lib/bomb'RSpec.describeBombdolet(:bomb){Bomb.new}describe'#detonate'doit'kills the developer'doexpect{bomb.detonate}.toraise_error(RuntimeError,'You are dead. No more coding.')endendend

Mocha + Chai

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// test/lib/bomb_test.jsimport'../test_helper'importBombfrom'../../lib/bomb'describe('Bomb',()=>{letbomb;beforeEach(()=>{bomb=newBomb;});describe('#detonate',()=>{it('kills the developer',()=>{expect(()=>{bomb.detonate();}).to.throw('You are dead. No more coding.');});});});

Properties like to, be, been, have, has etc. are there only for readability. They do not affect the assertions.

Overwriting dependencies

The RedButton has a very dangerous dependency on Bomb. We want to be able to test the button without actually detonating any bombs, so we need to get rid of the dangerous parts. In Ruby every class is accessible in the global scope, so that’s how we grab it and just rewrite its behavior.

Mocha + Proxyquire

In Javascript, every file imports modules to its own local scope. To overwrite a dependency, we have to fiddle with the import. Proxyquire is the tool for that job. It takes two arguments. The first one is the path to the module whose dependencies we will change (lib/red_button.js). This path is relative to the file where Proxyquire got imported, i.e. test/setup.js. The second one is a mapping of paths to objects overwriting exports. Those paths are relative to the file whose dependencies we’re rewriting, i.e. lib/red_button.js. Specifying {default:SafeBomb} means that the object exported with the name default (exports default class Bomb ...) will be swapped to SafeBomb . If there were any other exports in that file, they would not be affected. Proxyquire returns an object with all the exports from the imported file.

Mocha + Timekeeper

Custom assertions

One of my favorite things to do to tidy up the test suite is defining custom assertions. In this example, I will write one for asserting that a bomb is old. Let’s assume a bomb is old if it was produced before 1976 (that’s in bomb years! I am not saying the same condition applies to people). But does that mean that any bomb produced in or after 1976 is not old by our standards? I don’t think so. The line between being old and not being old is blurry. I’m going to assume that a bomb is not old if it’s been produced in 1996 ar later. That’s not a problem because both RSpec and Chai allow for a different boolean expression in the case of a negated assertion.

RSpec

Definition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# spec/support/matchers/old.rbrequire'rspec/expectations'RSpec::Matchers.define:be_olddomatchdo|actual|actual.production_date.year<1976endmatch_when_negateddo|actual|actual.production_date.year>=1996endfailure_messagedo|actual|"expected object to be produced before 1976, "\"but it was produced in #{actual.production_date.year}"endfailure_message_when_negateddo|actual|"expected object not to be produced before 1996, "\"but it was produced in #{actual.production_date.year}"endend

Import

1
2

# spec/spec_helper.rbrequire_relative'support/matchers/old'

Usage

1
2

# spec/lib/bomb_spec.rbexpect(bomb).tobe_old

Mocha + Chai

Definition

// test/support/assertions/old.jsexportdefaultfunction(_chai,utils){utils.addProperty(_chai.Assertion.prototype,'old',function(){letcondition,message,messageWhenNegated,expected,actual,subject=this._obj;if(utils.flag(this,'negate')){expected=1996;}else{expected=1976;}actual=subject.getProductionDate().getFullYear();condition=actual<expected;message='expected object to be produced before #{exp}, '+'but it was produced in #{act}';messageWhenNegated='expected object not to be produced before #{exp}, '+'but it was produced in #{act}';this.assert(condition,message,messageWhenNegated,expected,actual);});};

Import

Usage

In Chai, we can define an assertion as either a property or a method. There is an addMethod method for the latter. Method assertions are, obviously, used like so:

1

expect(bomb).to.be.old();

A really cool thing in Chai is flagging. Using utils.flag(), which is either a getter or a setter, we can flag an assertion in a chainable method, to later read that flag in another method, used further in the chain. negate is a built-in flag that gets set when the assertion chain includes not (expect(...).not.to.equal(...)). I recommend reading the documentation on Chai’s plugin concepts and plugin utilities.

Testing asynchronous methods

Something that rarely applies to Ruby, but almost always is needed in JavaScript.

Waiting for a callback

It is important to know that Mocha will not wait for all the callbacks to be executed. If this example below happens to be the only one to be run, it will pass. If other examples get run afterwards and thus there is enough time for this callback to be executed, it will fail. Do not do this!

1
2
3
4
5
6

it('will not wait the callback',()=>{// this test is unpredictable!setTimeout(()=>{expect(false).to.be.true;},1000);});

We can, however, force Mocha to wait. The it function can take an argument, usually called done, which is a method. Mocha will wait for the test suite to call this method before finishing running the example. We should call it inside the callback.

1
2
3
4
5
6
7
8

// test/lib/generic_test.jsimport'../test_helper'describe('Mocha',()=>{it('can wait for a callback',(done)=>{setTimeout(()=>{console.log('ding!');done();},1000);});});

It is also important to remember that a test taking longer than 2000ms will fail with a timeout.

Mocha reports the time of unusually slow tests.

1
2
3

Mocha
ding!
✓ can wait for a callback (1004ms)

Manually moving time forward with Sinon

// test/lib/bomb_test.jsimport'../test_helper'importBombfrom'../../lib/bomb'describe('Bomb',()=>{letbomb;context('manually controlled time',()=>{letclock;before(()=>{clock=sinon.useFakeTimers();});after(()=>{clock.restore();});it('kills the developer with a delay',()=>{bomb.detonate(10);expect(()=>{clock.tick(9)}).not.to.throw();expect(()=>{clock.tick(1)}).to.throw('You are dead. No more coding.');});});});

Source

If you want to mess around with the tests I wrote, they can be found here.

Conclusion

It’s really easy to get into testing JavaScript with Mocha if you already know RSpec. Chai has pretty awesome utilities for adding new assertions and extending existing ones. I would recommend using Mocha with Chai to my fellow RSpec fans, it feels really familiar.