loading...

Testing Node's Event Emitter

shelob9 profile image Josh Pollock ・4 min read

Today at work, I implemented a basic event system using Node's EventEmitter class. I am developing a state management system to aid in migrating a part of a WordPress plugin's admin interface from jQuery and Handlebars to React.

Our immediate goal is to fix some performance issues related to storing state in the DOM. I'm developing this API decoupled from the main Git repo for the plugin so I can work faster. This repo has unit tests, and the main git repo for the plugin will have acceptance tests proving that the system works in the plugin.

One type of test I'd never written before was to cover the events emitted with that event emitter. My logic is that if we're supposed to test our application like it is used, then we should test developer APIs like they are used. Use the comments to tell me any better approaches you may have.

Testing The Event Emitter

The plugin this is for is a form builder plugin. The code being tested manages the state of the forms in the form editor. In the test code below,

it('Emits remove field event', () => {
    // Mock the event callback to see that:
    const callback = jest.fn();

    // This is a factory function
    const state = fieldState([field]);

    //Bind jest's mock function to the event.
    state.feildEvents.on('removeField', fieldId => callback(fieldId));

    //Do the thing that triggers the event
    state.removeField(field.id);

    //Did callback get called once?  
    expect(callback).toBeCalledTimes(1);
    //Did callback get called with the right data?
    expect(callback).toBeCalledWith(field);
});

What I'm doing here is, instantiating my state management system, pretty much how it will actually get used, and then doing the thing that triggers the event and making sure the callback gets the right data.

It Could Be More Isolated

So, yes, this is more of an integration test than a unit test. I could have invoked the event directly. But, I didn't add that event emitter so I'd have something cute to write about on the internet. I needed to do something with the data the callback passes.

The test I wrote looks more like the actual code that is bound to the event I'm testing, than any unit test would look like. I don't care that an instance of EventEmitter emits events, I care that it emits the right event, with the right data at the right time.

I added the event emitter beacuse updating the field list wasn't updating the generated list of "magic tags" -- merge tags based on field values and other settings. So, really the most important tests here were showing that adding and removing fields change the total list of magic tags:

it('Adds field magic tag when adding a field', () => {
    // Start with 1 field
    const state = cfEditorState({
      intialFields: [
        {
         //...
        },
      ],
    });
    // Has one magic tag
    expect(state.getAllMagicTags().length).toBe(1);

    //Add a field
    state.addField({
      //...
    });

     // Has two magic tags
    expect(state.getAllMagicTags().length).toBe(2);
  });

This test covers that the event was fired, with the right data, and had the right effect. I have tests with less scope than that, which were helpful to have on the journey to getting this test to pass. But, this one final test makes sure everything works together.

Typing Events

This passed in my tests, but made compilation fail. The error I had was I was not extending the base event emitter. So when I exported the events object, TypeScript's compiler was not happy. A little googling later and I learned you can't export an internal in TypeScript.

And like, whatever, I found a solution on Github called tsee and it let's me type my events. That's really cool.

The one thing I don't like about event-based architecture is if the code emitting the event changes, the callback can break. This can happen accidently, beacuse it's not always obvious what is bound to an event. Typeing the events should help.

The one thing I don't like about event-based architecture is if the code emitting the event changes, the callback can break. This can happen accidently, beacuse it's not always obvious what is bound to an event. Typeing the events should help.

Before I made the change I had:

import EventEmitter from 'events';

I updated that to:

import { EventEmitter } from 'tsee';

Then I was able to type the event emitter:

const fieldEvents = new EventEmitter<{
    addField: (fieldId: fieldId) => void;
    updateField: (args: { fieldId: fieldId; beforeUpdate: field }) => void;
    removeField: (field: field) => void;
  }>();

A Fun Event

Event-based architecture is useful. Instead of piling more responsiblities into a function or file, sometimes it's better to open it to modification from the outside world. But, it also means the function has side effects.

Testing side effects is always difficult, in this post, I've shown one way to test that the data supplied to the callbacks is consistent. In this post you also learned how to use TypeScript to make it harder to make the kinds of mistakes this test prevents. Adding tests, and typing the events does not mean that they can't be consumed incorrectly, but it's a step in the right direction.

Featured image by Anthony Cantin on Unsplash

Discussion

pic
Editor guide