Aza
Aza

Reputation: 31

how to mock EventSource using jest with TS (tried most of the mocking strategy)

wanted to mock EventSource using jest, but kept throwing ReferenceError: EventSource is not defined.

Please have a look at the code. Thanks a lot!

// eventSourceHandler.ts
export default new class A() {
listenEventSource(){
    const eventSource = new EventSource(url);
    eventSource.addEventListener("something", callSomething);
    eventSource.onerror = function() {
      console.error();
      ("Failed to listen EventSource");
    };
}
}

Here is test code I want to mock

// eventSourceHandler.spec.ts

import A from "./eventSourceHandler"
describe("xyz",() =>{
it("eventSourceHandler called", ()=> {
const mEventSourceInstance = {
        addEventListener: jest.fn(),
        onerror: jest.fn(),
        close: jest.fn(),
        onmessage: jest.fn(),
        onopen: jest.fn(),
        url: "test-url",
        readyState: 0,
        withCredentials: false,
        CLOSED: 2,
        CONNECTING: 0,
        OPEN: 1,
        removeEventListener: jest.fn(),
        dispatchEvent: jest.fn()
      };
      jest.mock("EventSource", () => {
        return {
          EventSource: jest.fn().mockImplementation(() => {
            return {
              // addEventListener: jest.fn(),
              // onerror: jest.fn()
              mEventSourceInstance
            };
          })
        };
      });
      let a = new A()
      a.listenEventSource();
      // test validation ....
});
});
});
...

Kept getting ReferenceError: EventSource is not defined whenever run test code.

NOTE: I've read almost most related posts from stackoverflow and tried to mock global.EventSource but Typescript kept throwing error saying EventSource does not exist on type Global.

Is there anyone who wants to share a better mocking strategy for this? That will be highly appreciated.

Thanks guyzz ...

Upvotes: 3

Views: 7028

Answers (3)

Slava.In
Slava.In

Reputation: 1059

I was struggling with ReferenceError: EventSource is not defined for a while. But turns out official jest doc has pretty good explanation of what is up.

It's just that global objects are hoisted as soon as your import your piece of code that uses it and then can not be changed (kind of like a closure in my understanding).

So an easy work-around is to keep your definition of mocked global variables like EventSource in a separate module and import it before any other module that uses globals.

For instantce I have a module mockEventSource.ts with a following contents

Object.defineProperty(window, 'EventSource', {
  writable: true,
  value: jest.fn().mockImplementation(() => ({
    close: jest.fn(() => {}),
    addEventListener: jest.fn(
      (_event: string, _callback: (_message: MessageEvent) => {}) => {},
    ),
  })),
});

Then in my test file I do:

import './mockEventSource.ts';
import { myFunction } from './moduleToTest.ts';

And now when I call myFunction it should have an access to mocked EventSource

Upvotes: 4

Holly Cummins
Holly Cummins

Reputation: 11492

As @amir-raminfar suggests, eventsourcemock should do the trick. After npm install eventsourcemock, I added the following to my jest setup tests file (I was using create-react-app, so it was just ./setupTests.js for me:

import EventSource from 'eventsourcemock';

Object.defineProperty(window, 'EventSource', {
    value: EventSource,
});

Then, in the test, you would have something like

import {sources} from 'eventsourcemock';

const messageEvent = new MessageEvent('something', {
  data: '1',
});

sources[url].emit(
      messageEvent.type,
      messageEvent
    );
    expect(... your expected behaviour here);

The sources[url] will return a direct reference to the implementation the application code is using, so there's various things you can manipulate and inspect. In my tests, I couldn't get my onmessage listener to fire with emit, but if I called sources[url].onmessage directly with my test payload, that worked well.

Upvotes: 1

dleal
dleal

Reputation: 660

Well, I see two alternatives you might use.

  • Inject the eventSource instance in the class it's being used, so you can mock it
  • Use a builder function instead of calling the constructor directly in class A

For the latter, you could end up with something like this:

utils.ts

export const buildEventSource = (url: string) => {
  return new EventSource(url, {});
};

And then in your test class:

import * as utils from './utils';

const buildEventSourceSpy = jest.spyOn(utils, 'buildEventSource');

buildEventSourceSpy.mockReturnValue({
    CLOSED: 0,
    CONNECTING: 0,
    OPEN: 0,
    dispatchEvent(event: Event): boolean {
      return false;
    },
    onerror: jest.fn(),
    onmessage: jest.fn(),
    onopen: jest.fn(),
    readyState: 0,
    url: '',
    withCredentials: false,
    addEventListener(
      type: any,
      listener: any,
      options?: boolean | AddEventListenerOptions
    ): void {},
    close(): void {},
    removeEventListener(
      type: any,
      listener: any,
      options?: boolean | EventListenerOptions
    ): void {}
  });

I hope it helps

Upvotes: 4

Related Questions