Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to test a Server Sent Events (SSE) route in NodeJS?

I have a Server Sent Events route on my NodeJS app that clients can subscribe to for getting real-time updates from the server. It looks like follows:

router.get('/updates', (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    })

    const triggered = (info) => {
        res.write(`\ndata: ${JSON.stringify(info)}\n\n`)
    }

    eventEmitter.addListener(constants.events.TRIGGERED, triggered)

    req.on('close', () => {
        eventEmitter.removeListener(constants.events.TRIGGERED, triggered)
    })
})

Testing a traditional route using supertest is simple enough in node:

test('Should get and render view', async() => {
    const res = await request(app)
        .get('/')
        .expect(200)

    expect(res.text).not.toBeUndefined()
})

However, this does not work when testing a SSE route.

Does anyone have any ideas on how to test a SSE route with Node? It doesn't necessarily have to be tested with supertest. Just looking for ideas on how to test it, supertest or otherwise.

EDIT: I have an idea about how to integration test this. Basically, one would have to spin up a server before the test, subscribe to it during the test and close it after the test. However, it doesn't work as expected in Jest when I use beforeEach() and afterEach() to spin up a server.

like image 823
philosopher Avatar asked Jan 27 '20 18:01

philosopher


People also ask

How do you test SSE events?

To test it, you will normally open browser, making a connection to SSE channel and then wait for the push notification to receive on the browser when an event is triggered on the server. To test with multiple clients, you might open multiple browsers and do the same tests.

What is Server-Sent Events node JS?

Server-Sent Events (SSE) is a Web standard that allows a server application to send data to the client. Concretely, a browser can subscribe to a stream of events generated by a server, receiving updates whenever a new event occurs. This feature makes it possible to build a more reactive application.

What is EventSource in Nodejs?

The EventSource interface is web content's interface to server-sent events. An EventSource instance opens a persistent connection to an HTTP server, which sends events in text/event-stream format. The connection remains open until closed by calling EventSource.


Video Answer


2 Answers

I would mock/fake everything used by the endpoint, and check if the endpoint executes in the right order with the correct variables. First, I would declare trigger function and close event callback outside of the endpoint so that I could test them directly. Second, I would eliminate all global references in all functions in favor of function parameters:

let triggered = (res) => (info) => {
    res.write(`\ndata: ${JSON.stringify(info)}\n\n`);
}

let onCloseHandler = (eventEmitter, constants, triggered, res) => () => {
    eventEmitter.removeListener(constants.events.TRIGGERED, triggered(res));
}

let updatesHandler = (eventEmitter, constants, triggered) => (req, res) => {
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    eventEmitter.addListener(constants.events.TRIGGERED, triggered(res));

    req.on('close', onCloseHandler(eventEmitter, constants, triggered, res));
};

router.get('/updates', updatesHandler(eventEmitter, constants, triggered));

With this code, the test cases would be like:

test("triggered", () => {
    let res;

    beforeEach(() => {
        res = generateFakeRespone();
    });

    it("should execute res.write with the correct variable", () => {
        trigger(res)("whatever");

        expect(res.write).to.have.been.called.once;
        expect(res.write).to.have.been.called.with(`\ndata: ${JSON.stringify("whatever")}\n\n`);
    });
});


test("onCloseHandler", () => {
    let res;
    let eventEmitter;
    let constants;
    let triggered;

    beforeEach(() => {
        res = Math.random();
        eventEmitter = generateFakeEventEmitter();
        constants = generateFakeConstants();
        triggered = generateFakeTriggered();
    });

    it("should execute eventEmitter.removeListener", () => {
        onCloseHandler(eventEmitter, constants, triggered, res);

        expect(eventEmitter.removeListener).to.have.been.called.once;
        expect(eventEmitter.removeListener).to.have.been.called.with(/*...*/)
    });
});

test("updatesHandler", () => {
    beforeEach(() => {
        req = generateFakeRequest();
        res = generateFakeRespone();
        eventEmitter = generateFakeEventEmitter();
        constants = generateFakeConstants();
        triggered = generateFakeTriggered();
    });

    it("should execute res.writeHead", () => {
        updatesHandler(eventEmitter, constants, triggered)(req, res);

        expect(res.writeHead).to.have.been.called.once;
        expect(res.writeHead).to.have.been.called.with(/*...*/)
    });

    it("should execute req.on", () => {
        //...
    });

    // more tests ...
});

With this style of coding and testing, you have the ability to make very detailed unit test. The downside is that it take much more effort to test everything properly.

like image 50
kkkkkkk Avatar answered Oct 25 '22 06:10

kkkkkkk


Have a look at the tests for the express-sse library. They spin up the server on a port, then create an instance of EventSource and connect it to the SSE end-point on that running server.

Something like this:

describe("GET /my-events", () => {

  let events
  let server
  beforeEach(function (done) {
    events = new EventEmitter()
    const app = createMyApp(events)
    server = app.listen(3000, done)
  })

  afterEach(function (done) {
    server.close(done)
  })

  it('should send events', done => {
    const es = new EventSource('http://localhost:3000/my-events')
    
    events.emit('test', 'test message')
    es.onmessage = e => {
      assertThat(e.data, equalTo('test message'))
      es.close()
      done()
    }
  })
})

That seems like the right way to test it, to me.

like image 42
Matt Wynne Avatar answered Oct 25 '22 06:10

Matt Wynne