State Change
Testing for a state change is easy, and you can just run your action like a function.
Copy 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:
Copy 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.
actions.spec.js actions.js todoFx.js
Copy 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();
}
},
});
});
});
Copy import { effects } from 'ferp';
import * as todoFx from './todoFx.js';
export const AddTodo = (todo) => (state) => [
{ ...state, todos: state.todos.concat(todo) },
effects.none(),
];
export const RequestTodo = (todoId) => (state) => [
state,
todoApi.getTodo(state.external.fetch, todoId, AddTodo),
];
Copy import { effects } from 'ferp';
export const getTodo = (fetchFn, id, onSuccess, onFailure) => effects.defer(
fetchFn(`https://jsonplaceholder.typicode.com/todos/${id}`)
.then(response => response.json())
.then(data => effects.act(onSuccess(data)))
.catch((err) => (
onFailure
? effects.act(onFailure(err))
: effects.none()
))
);
Yes, that test code looks clunky, so why not make a helper to make things a little easier?
Copy 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:
actions.spec.js
Copy 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.