rhizo

1.2.0 • Public • Published

Rhizo

pipeline status

Rhizo is a simple runner for composable test fixtures you write. If you have tests which work with external concerns, especially databases, you're familiar with setting up fixtures in order to test processes which expect existing data. The simplest way to accommodate these tests is to tailor a fixture method to each functional area, but this quickly becomes impractical to maintain. By breaking down fixtures into small reusable components, you can build up the state each test requires without resorting to unsustainable copying and pasting.

Rhizo is framework-agnostic and will work with any test framework which allows you to set up hooks which run before a test or suite.

For more theoretical background, see this post. Rhizo implements the technique described there with a few differences to make it easier to apply as a standalone dependency.

Installation

npm i rhizo

Defining Fixtures

An ideal test fixture manages one and only one source datum (or small related collection thereof), and is composed with other single-responsibility fixtures to build the final state.

If, for example, you're writing tests for a hotel reservation system, you might have a fixture that inserts a guest, a second which inserts some rooms, and a third which adds a reservation. Your tests which concern guest profiles need only set up a stateFactory with the guests fixture, while tests evaluating calendar functionality require the reservations fixture to run after the other two -- and use the data they generate.

A fixture is an async or Promise-returning method, taking an environment which provides database connections and other such utilities, and a state which the fixture may read and write to. The state is passed in sequence from fixture to fixture.

Fixtures can be collected in an object but are best removed to a module or even a set of modules, ensuring easy access from all project test suites. The key corresponding to a fixture method will be written to in the state as the method executes: the guests fixture below will place the return value of the insert call in state.guests when executed, and so on.

exports = module.exports = {
  guests: (environment, state) => {
    return environment.db.guests.insert([{
      name: 'Jan Smith'
    }]);
  },
  rooms: (environment, state) => {
    return environment.db.rooms.insert([{
      number: 101,
      smoking: false
    }, {
      number: 102,
      smoking: false
    }]);
  },
  reservations: (environment, state) => {
    return environment.db.reservations.insert([{
      guest_id: state.guests[0].id,
      room_id: state.rooms[0].id,
      checkin_at: new Date('7/7/2018'),
      checkout_at: new Date('7/10/2018')
    }])
  }
};

A slightly neater organizational strategy involves breaking each fixture out into a module which exports the fixture method. The fixtures can be aggregated by dynamic requires:

const glob = require('glob');
 
exports.fixtures = glob.sync('test/helpers/fixtures/*.js').reduce((fixtures, file) => {
  fixtures[path.basename(file, '.js')] = require(path.resolve(file));
 
  return fixtures;
}, {});

The StateFactory

Invoke rhizo in a pre-test hook with the fixtures collection.

describe('a test suite', async function () {
  let stateFactory;
  let state;
 
  before(async function () {
    stateFactory = await rhizo(fixtures);
  });
});

The stateFactory Rhizo returns is a function taking two or more arguments. The first is an environment allowing you to pass in database connections and other static resources for use by the fixtures. All subsequent arguments are fixtures to be run in sequence. Each fixture is passed the environment and a state object ({} by default), which latter it modifies and passes on to the next fixture, building the completed state bit by bit.

The fixtures passed to the stateFactory are keys in the fixtures collection, but you can also inline fixture functions as shown below. These are identical to other fixtures in that they're async or Promise-returning functions which take an environment and state just like other fixtures. However, since they do not have names, they must explicitly modify and return the state.

  beforeEach(async function () {
    state = await stateFactory({
        db: db
      },
      'guests',
      'rooms',
      'reservations',
      async function adHocFixture(environment, state) {
        state.thing = 'stuff';
 
        return state;
      }
    );
 
    assert.lengthOf(state.guests, 1);
    assert.lengthOf(state.rooms, 2);
    assert.lengthOf(state.reservations, 1);
  });

state (and any database or other external changes behind it) can now be used in your tests.

Overriding and Passing States

The initial state is an empty object by default, but the environment may specify a state property of its own. If present, environment.state overrides the default. This allows states to be passed through multiple stateFactories, by setting the environment.state of the second invocation to the state output by the first. This is useful with nested describes with Mocha, for example: the outer beforeEach runs A, B, and C fixtures; then an inner beforeEach can leverage the work of the outer to run the D and E fixtures against the ABC state.

describe('outer', function () {
  let outerState;
 
  beforeEach(async function () {
    outerState = await stateFactory({
        db: db
      },
      'guests',
      'rooms'
    );
  });
 
  describe('inner', function () {
    beforeEach(async function () {
      state = await stateFactory({
          state: outerState,
          db: db
        },
        'reservations'
      );
    });
  });
});

Package Sidebar

Install

npm i rhizo

Weekly Downloads

0

Version

1.2.0

License

BSD-3-Clause

Unpacked Size

16.9 kB

Total Files

8

Last publish

Collaborators

  • dmfay