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) => {
      app({
        init: [
          null,
          notificationEffect(title, options, onComplete),
        ],
      });
      
    };
    
    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();
      });
    });
  });
});

Last updated