如何在NodeJS中测试服务器发送事件(SSE)路由?

8

我在我的NodeJS应用程序上有一个服务器发送事件(SSE)路由,客户端可以订阅该路由以获取来自服务器的实时更新。它如下所示:

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)
    })
})

在 Node 中使用 supertest 测试传统路由很简单:

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

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

然而,这种方法在测试SSE路由时无法使用。

有人有什么想法如何使用Node测试SSE路由吗?不一定要使用 supertest 进行测试。只是想寻找测试它的方法,不管是用 supertest 还是其他方法。

编辑: 我有一个想法来进行集成测试。基本上,在测试之前需要启动服务器,测试期间订阅它并在测试结束后关闭它。但是,当我使用 beforeEach() 和 afterEach() 来启动服务器时,在 Jest 中它并不能像预期的那样工作。

2个回答

4
我会模拟/伪造端点使用的所有内容,并检查端点是否按照正确的顺序执行并使用了正确的变量。首先,我会在端点外声明trigger函数和close事件回调,以便可以直接测试它们。其次,我会消除所有函数中的全局引用,而是优先使用函数参数:
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));

使用这段代码,测试用例将会如下:

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 ...
});

采用这种编程和测试方式,您可以制作非常详细的单元测试。缺点是需要更多的精力来正确地测试每个部分。


谢谢您的回复。测试方法非常有趣,但这会在不同位置重复许多路由中的代码。如果有人更改路由中的代码,除非他们也将新代码复制粘贴到测试中,否则这些测试将继续通过。 - philosopher
你的意思是如果 onCloseHandler 改变了,那么对于 updatesHandler 的测试仍然能够通过吗? - kkkkkkk
1
啊,好的,抱歉,我再仔细读了一遍。你建议我重构我的代码,将我的路由分成多个函数,然后导出这些函数,以便可以单独测试它们。这似乎是一个有效的方法,但这意味着我正在将它们重构为多个函数,仅仅是为了进行测试。这不会被认为是不良实践吗?例如,当我使用supertest测试其他路由时,我的任何路由都不需要拆分成多个函数。 - philosopher
这并不是一种糟糕的做法,而是代码看起来更加清晰的方式。以这种风格编码可以确保你的代码在函数命名合适的情况下部分自我记录。此外,每个函数将变得更小,全局引用更少,责任更少,这应该使它们更容易理解。 - kkkkkkk

1
请查看express-sse库的测试。它们在端口上启动服务器,然后创建一个EventSource实例并将其连接到运行服务器上的SSE端点。
类似于这样:
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()
    }
  })
})

那对我来说似乎是正确的测试方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接