rhizo
v1.2.0
Published
a framework-agnostic runner for composable test fixtures
Downloads
1
Maintainers
Readme
Rhizo
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 require
s:
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 describe
s 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'
);
});
});
});