Testing Actions

State Change

Testing for a state change is easy, and you can just run your action like a function.

import { effects } from 'ferp';

const IncrementCounter = state => [
  { ...state, counter: state.counter + 1 },
  effects.none(),
];

describe('IncremenetCounter', () => {
  it('increments the counter state variable', () => {
    const initialState = { counter: 0 };
    
    const [state, _effect] = IncrementCounter(initialState);
    
    expect(state).toDeepEqual({
      counter: 1,
    });
  });
});

Testing action builders is very similar, too:

import { effects } from 'ferp';

const IncrementCounterByN = n => state => [
  { ...state, counter: state.counter + n },
  effects.none(),
];

describe('IncremenetCounterByN', () => {
  it('increments the counter state variable using the provided value', () => {
    const initialState = { counter: 0 };
    
    const [state, _effect] = IncrementCounterByN(999)(initialState);
    
    expect(state).toDeepEqual({
      counter: 999,
    });
  });
});

Result of Side-Effects

Testing that the correct side effects can be a little more tricky. The problem is that effects touch the outside world that is not controlled by Ferp. Running these end-to-end just like the effect testing is likely your best bet.

import * as sinon from 'sinon';
import { app } from 'ferp';

import * as actions from './actions.js';

describe('RequestTodo', () => {
  it('does not modify the state', () => {
    const initialState = { todos: [], externals: { fetch: window.fetch } };
    
    const [state] = actions.RequestTodo(1)(initialState);
    
    expect(state).toBe(initialState);
  });
  
  it('successfully fetches a todo', (done) => {
    const expectedTodo = { id: 1, text: 'foo', completed: false };
    const fakeFetch = sinon.fake((arg) => {
      return {
        json: Promise.resolve(expectedTodo)
      };
    });
    const initialState = { todos: [], externals: { fetch: fakeFetch } };
    
    const expectedTodosInOrder = [
      [], // init
      [], // request
      [expectedTodo], // on success
    ];
    
    app({
      init: actions.RequestTodo(expectedTodo.id)(initialState),
      observe: ([state, effect]) => {
        expect(state.todos)
          .toDeepEqual(expectedTodosInOrder.unshift());
          
        if (expectedTodosInOrder.length === 0) {
          done();
        }
      },
    });
  });
});

Yes, that test code looks clunky, so why not make a helper to make things a little easier?

support/endToEndTest.js
import { app } from 'ferp';

export const endToEndTest = (init, assertions, done) => app({
  init,
  observe: (...args) => {
    const assertion = assertions.unshift();
    assertion(...args);
    if (assertions.length === 0) {
      done();
    }
  },
});

Which would result in:

import { endToEndTest } from './support/endToEndTest.js';

// ...

  it('successfully fetches a todo', (done) => {
    const expectedTodo = { id: 1, text: 'foo', completed: false };
    const fakeFetch = sinon.fake((arg) => {
      return {
        json: Promise.resolve(expectedTodo)
      };
    });
    const initialState = { todos: [], externals: { fetch: fakeFetch } };
    
    endToEndTest(
      actions.RequestTodo(expectedTodo.id)(initialState),
      [
        ([state]) => expect(state.todos).toDeepEqual([]), // init
        ([state]) => expect(state.todos).toDeepEqual([]), // request
        ([state]) => expect(state.todos).toDeepEqual([expectedTodo]), // on success
      ],
      done
    );
  });

There may be other forms of helpers that can make the end-to-end portions of your tests to be more readable and manageable.

Last updated