Testing Your Effects

The Problem

The problem you will initially run into is this: I just want to test an effect, but I don't know how to run the effect on its own. Well, maybe you don't need to run the effect in isolation, and run it through a test app. This will keep your test reflecting the reality of how it is used, while also helping you isolate how the effect affects the state.

The Solution: End to End

Let's take the Custom Effects section example of notificationPermission and notification effects.

notificationEffects.js
import { effects } from 'ferp'
export const notificationPermissionEffect = (
grantEffect,
denyEffect = effects.none,
dismissEffect = effects.none,
errorEffect = effects.none
) => effects.defer(
Notification.requestPermission()
.then((result) => {
switch (result) {
case 'granted':
return grantEffect();
case 'denied':
return denyEffect();
default:
return dismissEffect();
}
})
.catch(errorEffect)
);
export const notificationEffect = (title, options = {}, resultEffect = effects.none) => effects.thunk(() =>
resultEffect(new Notification(title, options))
);

In this case, we have some browser specific code (ie the Notification class), so we'll just make a global/controllable mock for it. If you are building a browser-based ferp app, you may want to look into a modern browser based test framework. Let's set up a test using describe/it blocks, similar to what you'd seen in mocha or jest:

notificationEffects.test.js
import { app, effects } from 'ferp';
import { notificationPermissionEffect, notificationEffect } from './notificationEffects.js';
describe('Notification effects', () => {
describe('notificationPermissionEffect', () => {
const injectNotificationClass = (permissionResponse) => {
global.Notification = class {
static requestPermission() {
return permissionResponse;
}
}
};
afterEach(() => {
delete global.Notification;
});
const createTestApp = (onComplete) => {
const setterEffect = (value) => () => value;
const detach = app({
init: [
null,
notificationPermissionEffect(
setterEffect('grantEffect'),
setterEffect('denyEffect'),
setterEffect('dismissEffect'),
setterEffect('errorEffect'),
),
],
update: (message, state) => {
onComplete(message);
detach();
return [state, effects.none()];
},
});
};
it('calls the grantEffect', (done) => {
injectNotificationClass(Promise.resolve('grant'));
createTestApp((whichEffect) => {
expect(whichEffect).toBe('grantEffect');
done();
});
});
it('calls the denyEffect', (done) => {
injectNotificationClass(Promise.resolve('deny'));
createTestApp((whichEffect) => {
expect(whichEffect).toBe('denyEffect');
done();
});
});
it('calls the dismissEffect', (done) => {
injectNotificationClass(Promise.resolve('dismiss'));
createTestApp((whichEffect) => {
expect(whichEffect).toBe('dismissEffect');
done();
});
});
it('calls the errorEffect', (done) => {
injectNotificationClass(Promise.reject());
createTestApp((whichEffect) => {
expect(whichEffect).toBe('errorEffect');
done();
});
});
});
describe('notificationEffect', () => {
const injectNotificationClass = () => {
global.Notification = class {
constructor(title, options) {
this.constructorArgs = [title, options];
}
}
};
afterEach(() => {
delete global.Notification;
});
const createTestApp = (title, options, onComplete) => {
const detach = app({
init: [
null,
notificationEffect(title, options, onComplete),
],
update: (message, state) => {
detach();
return [state, effects.none()];
},
});
};
it('creates a notification', (done) => {
injectNotificationClass();
createTestApp('Hello world', { body: 'From Ferp.JS' }, (notification) => {
expect(notification).toBeInstanceOf(Notification);
expect(notification.constructorArgs).toEqual(['Hellow world', { body: 'From Ferp.JS' }]);
done();
return effects.none();
});
});
});
});