Skip to content

Commit

Permalink
Make tests less fragile / more readable
Browse files Browse the repository at this point in the history
  • Loading branch information
kenkunz committed Nov 8, 2021
1 parent 7813ddc commit 995a07c
Showing 1 changed file with 52 additions and 89 deletions.
141 changes: 52 additions & 89 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,35 @@ import sinon from 'sinon';
import fsm from './index.js';

describe('a finite state machine', () => {
let machine, kickHandler, subscribeHandler, actionHandler;
let states, machine;

beforeEach(() => {
kickHandler = sinon.stub();
subscribeHandler = sinon.stub();
actionHandler = sinon.stub();

machine = fsm('off', {
states = {
'*': {
_exit: actionHandler.bind(null, '*:_exit'),
_exit: sinon.stub(),
surge: 'blown-default',
poke: actionHandler.bind(null, 'hey!')
poke: sinon.stub()
},

off: {
_enter: actionHandler.bind(null, 'off:_enter'),
_exit: actionHandler.bind(null, 'off:_exit'),
_enter: sinon.stub(),
_exit: sinon.stub(),
toggle: 'on',
surge: 'blown',
kick: kickHandler,
subscribe: subscribeHandler,
kick: sinon.stub(),
subscribe: sinon.stub(),
arrowFunction: () => {
this.shouldExplode();
}
},

on: {
_enter: actionHandler.bind(null, 'on:_enter'),
_enter: sinon.stub(),
toggle: 'off'
}
});
};

machine = fsm('off', states);
});

afterEach(() => {
Expand All @@ -51,34 +49,24 @@ describe('a finite state machine', () => {
const callback = sinon.stub();
const unsubscribe = machine.subscribe(callback);
assert.isTrue(callback.calledOnce);
assert.equal('off', callback.firstCall.args[0]);
assert.isTrue(callback.calledWithExactly('off'));
unsubscribe();
});

it('should return unsubscribe function when invoked with callback', () => {
assert.isFunction(machine.subscribe(sinon.fake()));
});

it('should call subscribe action handler when invoked with no args', () => {
machine.subscribe();
assert.isTrue(subscribeHandler.calledOnce);
assert.isEmpty(subscribeHandler.firstCall.args);
assert.isTrue(states.off.subscribe.calledOnce);
});

it('should call subscribe action handler when invoked with single non-function arg', () => {
machine.subscribe('not a function');
assert.isTrue(subscribeHandler.calledOnce);
assert.lengthOf(subscribeHandler.firstCall.args, 1);
assert.equal('not a function', subscribeHandler.firstCall.args[0]);
assert.isTrue(states.off.subscribe.calledWithExactly('not a function'));
});

it('should call subscribe action handler when invoked with multiple args', () => {
const fn = sinon.fake()
machine.subscribe(fn, null);
assert.isTrue(subscribeHandler.calledOnce);
assert.lengthOf(subscribeHandler.firstCall.args, 2);
assert.equal(fn, subscribeHandler.firstCall.args[0]);
assert.isNull(subscribeHandler.firstCall.args[1]);
assert.isTrue(states.off.subscribe.calledWithExactly(fn, null));
});
});

Expand All @@ -89,113 +77,90 @@ describe('a finite state machine', () => {
beforeEach(() => {
callback = sinon.stub();
unsubscribe = machine.subscribe(callback);
callback.resetHistory();
});

afterEach(() => {
unsubscribe();
});

it('should silently handle unregistered actions', () => {
machine.noop();
assert.isTrue(callback.calledOnce);
assert.equal('off', machine.noop());
assert.isTrue(callback.notCalled);
});

it('should invoke registered action functions', () => {
machine.kick();
assert.isTrue(kickHandler.calledOnce);
assert.isTrue(states.off.kick.calledOnce);
});

it('should transition to static value registered action', () => {
machine.toggle();
assert.isTrue(callback.calledTwice);
assert.equal('on', callback.secondCall.args[0]);
assert.equal('on', machine.toggle());
assert.isTrue(callback.calledWithExactly('on'));
});

it('should not transition if invoked action returns nothing', () => {
machine.kick();
assert.isTrue(callback.calledOnce);
assert.equal('off', callback.firstCall.args[0]);
assert.equal('off', machine.kick());
assert.isTrue(callback.notCalled);
});

it('should transition to invoked action return value', () => {
kickHandler.returns('on');
machine.kick();
assert.isTrue(callback.calledTwice);
assert.equal('on', callback.secondCall.args[0]);
});

it('should return resulting state', () => {
assert.equal('on', machine.toggle());
states.off.kick.returns('on');
assert.equal('on', machine.kick());
assert.isTrue(callback.calledWithExactly('on'));
});

it('should pass arguments through to invoked action', () => {
kickHandler.withArgs('hard').returns('on');

machine.kick();
assert.isTrue(callback.calledOnce);
assert.equal('off', callback.firstCall.args[0]);

machine.kick('hard');
assert.isTrue(callback.calledTwice);
assert.equal('on', callback.secondCall.args[0]);
assert.isTrue(states.off.kick.calledWithExactly('hard'));
});

it('should bind `this` to state proxy object on invoked actions', () => {
machine.kick();
assert.isTrue(kickHandler.calledOn(machine));
assert.isTrue(states.off.kick.calledOn(machine));
});

it('should not bind `this` on actions defined as arrow functions', () => {
assert.throws(machine.arrowFunction, TypeError);
});

it('should not notify subscribers when state unchanged', () => {
kickHandler.returns('off');
machine.kick();
assert.isTrue(callback.calledOnce);
});

it('should call lifecycle actions in proper sequence', () => {
callback.callsFake(actionHandler);
machine.toggle();
assert.equal(4, actionHandler.callCount);
assert.equal('off:_enter', actionHandler.firstCall.args[0]);
assert.equal('off:_exit', actionHandler.secondCall.args[0]);
assert.equal('on', actionHandler.thirdCall.args[0]);
assert.equal('on:_enter', actionHandler.getCall(3).args[0]);
assert.isTrue(states.off._enter.calledBefore(states.off._exit));
assert.isTrue(states.off._exit.calledBefore(callback));
assert.isTrue(callback.calledBefore(states.on._enter));
});

it('should call _enter with appropirate metadata when fsm is created', () => {
const metadata = { from: null, to: 'off', event: null, args: [] };
assert.isTrue(states.off._enter.calledWithExactly(metadata));
});

it('should call lifecycle actions with transition metadata', () => {
const initial = { from: null, to: 'off', event: null, args: [] };
const transition = { from: 'off', to: 'on', event: 'toggle', args: [1, 'foo'] };
const metadata = { from: 'off', to: 'on', event: 'toggle', args: [1, 'foo'] };
machine.toggle(1, 'foo');
assert.deepEqual(initial, actionHandler.firstCall.args[1]);
assert.deepEqual(transition, actionHandler.secondCall.args[1]);
assert.deepEqual(transition, actionHandler.thirdCall.args[1]);
assert.isTrue(states.off._exit.calledWithExactly(metadata));
assert.isTrue(states.on._enter.calledWithExactly(metadata));
});

it('should not throw error when no matching state node', () => {
machine.surge();
assert.isTrue(callback.calledTwice);
assert.equal('blown', callback.secondCall.args[0]);
assert.isTrue(callback.calledWithExactly('blown'));
assert.doesNotThrow(() => machine.toggle());
});

it('should invoke fallback actions if no match on current state', () => {
actionHandler.reset();
machine.poke();
assert.isTrue(states['*'].poke.called);
machine.toggle();
const state = machine.surge();
assert.equal('blown-default', state);
assert.equal(4, actionHandler.callCount);
assert.equal('hey!', actionHandler.firstCall.args[0]);
assert.equal('*:_exit', actionHandler.lastCall.args[0]);
assert.equal('blown-default', machine.surge());
assert.isTrue(states['*']._exit.called);
});

it('should stop notifying after unsubscribe', () => {
unsubscribe();
machine.toggle();
assert.isTrue(callback.calledOnce);
assert.isTrue(callback.notCalled);
});
});

Expand All @@ -218,27 +183,25 @@ describe('a finite state machine', () => {
const debouncedKick = machine.kick.debounce(100);
clock.tick(100);
await debouncedKick;
assert.isTrue(kickHandler.calledOnce);
assert.isTrue(states.off.kick.calledOnce);
});

it('should pass arguments through to action', async () => {
const debouncedKick = machine.kick.debounce(100, 'hard');
clock.tick(100);
await debouncedKick;
assert.isTrue(kickHandler.calledOnce);
assert.equal('hard', kickHandler.firstCall.args[0]);
assert.isTrue(states.off.kick.calledWithExactly('hard'));
});

it('should debounce multiple calls within wait time', async () => {
const firstKick = machine.kick.debounce(100, 1);
clock.tick(50);
const secondKick = machine.kick.debounce(100, 2);
clock.tick(50);
assert.isTrue(kickHandler.notCalled);
assert.isTrue(states.off.kick.notCalled);
clock.tick(50);
await secondKick;
assert.isTrue(kickHandler.calledOnce);
assert.equal(2, kickHandler.firstCall.args[0]);
assert.isTrue(states.off.kick.calledWithExactly(2));
});

it('should invoke action after last call’s wait time', async () => {
Expand All @@ -247,8 +210,8 @@ describe('a finite state machine', () => {
const secondKick = machine.kick.debounce(10, 2);
clock.tick(10);
await secondKick;
assert.isTrue(kickHandler.calledOnce);
assert.equal(2, kickHandler.firstCall.args[0]);
assert.isTrue(states.off.kick.calledOnce);
assert.isTrue(states.off.kick.calledWithExactly(2));
});
});
});

0 comments on commit 995a07c

Please sign in to comment.