Links

Bus - the module for JavaScript developers

TL;DR: the proper use of the Bus module can solve the problem with dependencies between other modules, and ease the task of both unit and integration testing.

Scenario

Let's suppose there's a JavaScript appliction. It is composed of many modules, including the modules userA, userB and userC. I do realise, that the names aren't very smart, but it doesn't matter. The only fact that matters is they are all distinct modules.

That's not all, though. The module userA uses the module moduleA. To be more precise, it uses the functions a and b from that module. The module userB uses the function moduleA.b, and the module userC uses the function moduleA.c.

It's not very complicated: there are four modules, and three of them are using the fourth one.

Let's take a look at tests for the module moduleA:

var moduleA = require('moduleA');

//...

it('function a', () => {
  // perform some assertions
});

It can be assumed, that all the functions moduleA.a, moduleA.b and moduleA.c are pure, so they can be easily tested in full isolation. Thus, unit testing of the moduleA is not a problem.

In the unit test for the module userA, two mocks (or stubs) are required: one for moduleA.a, and one for moduleB.b. Below are presented code snippets of the tests:

var moduleA = require('moduleA'),
    sinon   = require('sinon');

var stub_a, stub_b;

beforeEach( () => {
  // set up the stubs
  stub_a = sinon.stub(moduleA, 'a');
  stub_b = sinon.stub(moduleA, 'b');
});

afterEach( () => {
  // remove the stubs
  moduleA.a.remove();
  moduleA.b.remove();
});

it('some test', () => {
  // configure the stubs
  // do something
  // perform all the checks
});

Unit tests for the modules userB and userC will be very similar.

In the integration tests for the modules userA, userB and userC, no stubs for functions from the module moduleA will be used.

The change

Now, let's assume that a simple change is needed. The functions b and c have to be removed from the module moduleA and go to the new, separate modules moduleB and moduleC. Such a split happens, especially when some module grows too much.

How this change will affect the unit tests for the modules moduleA, moduleB and moduleC?

Well, some tests from the moduleA should go to the modules moduleB and moduleC, since some functions have been moved to the new modules. It's quite simple and expected change.

Dependencies problem

After the change, the situation of application is different. There are six modules, instead of just four. Now, the module userA uses functions from two modules: moduleA.a and moduleB.b. The module userB uses the function b from the module moduleB, and the module userC uses the function moduleC.c.

Those changes are big, and quite unexpected. It was the module moduleA that had been changed, and the change affected all the modules, which depended on moduleA. Because of this change, the modules userA, userB and userC also have to be modified. Moreover, the change also affects the tests for those dependent modules.

This situation is very incovenient. A change in one module affected many other modules. It is a serious problem.

The Bus module

This problem can be solved with the Bus module.

Let's assume, that the module moduleA was written in a different way:

var Bus = require('bus');

exports.a = function() {
  // ...
};

exports.b = function() {
  // ...
};

exports.c = function() {
  // ...
};

exports.init = function() {
  Bus.on('a', exports.a);
  Bus.on('b', exports.b);
  Bus.on('c', exports.c);
};

Now, instead of just exporting the functions a, b and c, the module registers handlers for the Bus messages 'a', 'b' and 'c'. The new behaviour should be tested in the unit tests:

var moduleA = require('moduleA'),
    Bus     = require('bus'),
    sinon   = require('sinon');

//...

it('function a', () => {
  // perform some assertions
});

it('initialisation', () => {
  Bus.clear();
  let on = sinon.stub(Bus, 'on');
  moduleA.init();

  on.withArgs('a', moduleA.a).calledOnce.should.be.true();
  on.withArgs('b', moduleA.b).calledOnce.should.be.true();
  on.withArgs('c', moduleA.c).calledOnce.should.be.true();

  Bus.on.remove();
});

The bus messages 'a', 'b' and 'c' should be now used as an API by other modules, so it should be checked, whether the Bus.on function was called for those messages in order to register the proper handlers.

In the userA, userB and userC modules, the Bus module should be used, instead of the moduleA:

// in the module userA
var Bus = require('bus');

exports.someFunction = () => {
  let result_a = Bus.send('a'),
      result_b = Bus.send('b');
  // ...
};

Using the Bus module in this way, the modules userA, userB and userC become independent of the module moduleA. Of course, they become dependent on the module Bus, but this module is not supposed to be changed at all.

Testing

Since the modules userA, userB and userC now use the Bus module, instead of the moduleA, they should be tested in a different way.

As it is presented above, the module userA exports function someFunction. This function uses Bus to send messages 'a' and 'b' (and, probably, to get the results of those messages, which are supposed to come from the handlers registered in the moduleA). This functionality can be tested with stubs of the Bus.send function:

var userA = require('userA'),
    Bus   = require('bus'),
    sinon = require('sinon');

var send;

beforeEach( () => {
  Bus.clear();
  send = sinon.stub(Bus, 'send');
});

afterEach( () => {
  Bus.send.remove();
});

it('someFunction sends the messages', () => {
  userA.someFunction();

  send.withArgs('a').calledOnce.should.be.true();
  send.withArgs('b').calledOnce.should.be.true();
});

it('some function', () => {
  // configure the stub

  userA.someFunction();

  // perform some assertions
});

Instead of using the module moduleA, the Bus is used (and mocked). The only additional test checks, whether the tested function sends the proper messages.

The change (once again)

So, let's assume, that the moduleA is split into modules moduleA, moduleB and moduleC, in the same way as described in the beginning of this article.

Besides of the obvious code changes in the moduleA, the tests for this module can be analogically separated.

What's really interesting, though, is what happens to the modules userA, userB and userC. Since those modules are no longer dependent on the moduleA, they don't have to be changed at all. The unit tests for those modules also remain unchanged.

In such a way, one dependency problem has been solved with the Bus module.

Integration testing problem

The dependency problem hasn't been eliminated, though. In integration tests, all the modules are needed in order to test them together. Before the change was made, the ingeration tests required the module moduleA. Now, they require different modules: moduleA, moduleB and moduleC to be initialized.

This little inconvenience can be eliminated by introducing a new module, used solely for the purpose of initialization of all the modules:

exports.init = () => {
  require('moduleA').init();
  require('moduleB').init();
  require('moduleC').init();
};

This module should be used in all the integration tests:

var init = require('init-module'),
    Bus  = require('bus');

beforeEach( () => {
  Bus.clear();
  init.init();
});

Let's sum it up: now the modules userA, userB and userC require two modules for unit testing (userX and Bus), and two modules for integration testing (the same modules). Although the number of required modules seems not to change at all, the difference is significant. Now, every module could've been split into other modules, and still no changes would be required to the userA, userB and userC modules, and to their tests. Such software architecture is much easier to maintain and to extend.

Summary

As described above, the proper use of the Bus module can solve the problem with dependencies between other modules, and ease the task of both unit and integration testing.

The idea behind the Bus module is not new. It is an application of the well-known mediator pattern.

Copyright (C) Tomasz Primke 2015-2016