【问题标题】:Unit testing Node.js and WebSockets (Socket.io)单元测试 Node.js 和 WebSockets (Socket.io)
【发布时间】:2013-03-08 16:51:26
【问题描述】:

谁能使用 WebSockets (Socket.io) 为 Node.js 提供坚如磐石、极其简单的单元测试?

我将 socket.io 用于 Node.js,并查看了 socket.io-client 以在测试中建立与服务器的客户端连接。但是,我似乎遗漏了一些东西。

在下面的示例中,“worked...”永远不会被打印出来。

var io = require('socket.io-client')
, assert = require('assert')
, expect = require('expect.js');

describe('Suite of unit tests', function() {

    describe('First (hopefully useful) test', function() {

        var socket = io.connect('http://localhost:3001');
        socket.on('connect', function(done) {
            console.log('worked...');
            done();
        });

        it('Doing some things with indexOf()', function() {
            expect([1, 2, 3].indexOf(5)).to.be.equal(-1);
            expect([1, 2, 3].indexOf(0)).to.be.equal(-1);
        });

    });
});

相反,我只是得到:

  Suite of unit tests
    First (hopefully useful) test
      ✓ Doing some things with indexOf() 


  1 test complete (26 ms)

有什么建议吗?

【问题讨论】:

  • 这是摩卡咖啡还是茉莉花测试?对于异步 mocha 测试(就是这样),您的测试函数需要接受回调 function(testDone) 以便 mocha 知道要适当地对待它。这实际上可能有效,但 mocha 在 'connect' 事件触发之前退出,因为 mocha 不知道它应该等待。
  • Socket.io's docs 有 mocha、jest 和 tape 的示例。

标签: node.js unit-testing websocket socket.io


【解决方案1】:

经过进一步的戳戳,我发现了一些incredibly useful information。在作者的示例中,他指出了在 before 钩子中建立套接字侦听器的关键步骤。

这个例子有效:

当然,假设服务器正在localhost:3001 监听套接字连接

var io = require('socket.io-client')
, assert = require('assert')
, expect = require('expect.js');

describe('Suite of unit tests', function() {

    var socket;

    beforeEach(function(done) {
        // Setup
        socket = io.connect('http://localhost:3001', {
            'reconnection delay' : 0
            , 'reopen delay' : 0
            , 'force new connection' : true
        });
        socket.on('connect', function() {
            console.log('worked...');
            done();
        });
        socket.on('disconnect', function() {
            console.log('disconnected...');
        })
    });

    afterEach(function(done) {
        // Cleanup
        if(socket.connected) {
            console.log('disconnecting...');
            socket.disconnect();
        } else {
            // There will not be a connection unless you have done() in beforeEach, socket.on('connect'...)
            console.log('no connection to break...');
        }
        done();
    });

    describe('First (hopefully useful) test', function() {

        it('Doing some things with indexOf()', function(done) {
            expect([1, 2, 3].indexOf(5)).to.be.equal(-1);
            expect([1, 2, 3].indexOf(0)).to.be.equal(-1);
            done();
        });

        it('Doing something else with indexOf()', function(done) {
            expect([1, 2, 3].indexOf(5)).to.be.equal(-1);
            expect([1, 2, 3].indexOf(0)).to.be.equal(-1);
            done();
        });

    });

});

我发现done()beforeEachsocket.on('connect'...) 监听器中的位置对于建立连接至关重要。例如,如果您在侦听器中注释掉 done(),然后将其添加一个范围(就在退出 beforeEach 之前),您将看到 "no connection to break ..." 消息,而不是 "disconnecting..." 消息。像这样:

beforeEach(function(done) {
    // Setup
    socket = io.connect('http://localhost:3001', {
        'reconnection delay' : 0
        , 'reopen delay' : 0
        , 'force new connection' : true
    });
    socket.on('connect', function() {
        console.log('worked...');
        //done();
    });
    socket.on('disconnect', function() {
        console.log('disconnected...');
    });
    done();
});

我是 Mocha 的新手,因此将 done() 放置在套接字范围本身中可能有一个非常明显的原因。希望这个小细节能让其他人免于拉扯头发。

对我来说,上述测试(done() 的范围正确)输出:

  Suite of unit tests
    First (hopefully useful) test
      ◦ Doing some things with indexOf(): worked...
      ✓ Doing some things with indexOf() 
disconnecting...
disconnected...
      ◦ Doing something else with indexOf(): worked...
      ✓ Doing something else with indexOf() 
disconnecting...
disconnected...


  2 tests complete (93 ms)

【讨论】:

  • 在创建新套接字时将 {'forceNew': true} 添加到选项中。这样,您可以在单元测试中创建多个客户端套接字。
  • @mysterlune socket.on('connect'...) 范围内的 done() 确保套接字在测试开始运行之前连接。该操作将排队,直到在前一个上下文中调用 done()。
  • 你能展示一个客户端发出东西而服务器响应的测试示例吗?
【解决方案2】:

在此处提供已接受答案的扩展。具有基本的客户端到服务器通信,可用作其他未来测试的样板。使用 mocha、chai 和 expect。

var io = require('socket.io-client')
  , io_server = require('socket.io').listen(3001);

describe('basic socket.io example', function() {

  var socket;

  beforeEach(function(done) {
    // Setup
    socket = io.connect('http://localhost:3001', {
      'reconnection delay' : 0
      , 'reopen delay' : 0
      , 'force new connection' : true
      , transports: ['websocket']
    });

    socket.on('connect', () => {
      done();
    });

    socket.on('disconnect', () => {
      // console.log('disconnected...');
    });
  });

  afterEach((done) => {
    // Cleanup
    if(socket.connected) {
      socket.disconnect();
    }
    io_server.close();
    done();
  });

  it('should communicate', (done) => {
    // once connected, emit Hello World
    io_server.emit('echo', 'Hello World');

    socket.once('echo', (message) => {
      // Check that the message matches
      expect(message).to.equal('Hello World');
      done();
    });

    io_server.on('connection', (socket) => {
      expect(socket).to.not.be.null;
    });
  });

});

【讨论】:

  • 你能展示一个客户端发出东西而服务器响应的测试示例吗?
【解决方案3】:

自己处理回调和 Promise 可能很困难,而且不平凡的示例很快就会变得非常复杂且难以阅读。

有一个名为 socket.io-await-test 的工具可通过 NPM 获得,它允许您在测试中暂停/等待,直到使用 await 关键字触发事件。

  describe("wait for tests", () => {
    it("resolves when a number of events are received", async () => {
        const tester = new SocketTester(client);
        const pongs = tester.on('pong');
        
        client.emit('ping', 1);
        client.emit('ping', 2);
        await pongs.waitForEvents(2) // Blocks until the server emits "pong" twice. 

        assert.equal(pongs.get(0), 2)
        assert.equal(pongs.get(1), 3)
    })
})

【讨论】:

  • 这怎么得了0票?我见过的最优雅的解决方案。投我一票
  • 正是我想要的
  • OP here...自从原始帖子以来,所有单元测试都得到了很多改进。很高兴提升/突出此类以更好/现代方式回答问题的帖子。我自己会 1+ :)
【解决方案4】:

查看这个基于承诺良好实践样板解决方案。 您可以用它测试您的服务器的整个 io 事件,毫不费力。 您只需要复制样板测试并根据需要添加自己的代码。

查看 GitHub 上的存储库以获取完整源代码。

https://github.com/PatMan10/testing_socketIO_server

const io = require("socket.io-client");
const ev = require("../utils/events");
const logger = require("../utils/logger");

// initSocket returns a promise
// success: resolve a new socket object
// fail: reject a error
const initSocket = () => {
  return new Promise((resolve, reject) => {
      // create socket for communication
      const socket = io("localhost:5000", {
        "reconnection delay": 0,
        "reopen delay": 0,
        "force new connection": true
      });

      // define event handler for sucessfull connection
      socket.on(ev.CONNECT, () => {
        logger.info("connected");
        resolve(socket);
      });

      // if connection takes longer than 5 seconds throw error
      setTimeout(() => {
        reject(new Error("Failed to connect wihtin 5 seconds."));
      }, 5000);
    }
  );
};


// destroySocket returns a promise
// success: resolve true
// fail: resolve false
const destroySocket = socket => {
  return new Promise((resolve, reject) => {
    // check if socket connected
    if (socket.connected) {
      // disconnect socket
      logger.info("disconnecting...");
      socket.disconnect();
      resolve(true);
    } else {
      // not connected
      logger.info("no connection to break...");
      resolve(false);
    }
  });
};

describe("test suit: Echo & Bello", () => {
  test("test: ECHO", async () => {
    // create socket for communication
    const socketClient = await initSocket();

    // create new promise for server response
    const serverResponse = new Promise((resolve, reject) => {
      // define a handler for the test event
      socketClient.on(ev.res_ECHO, data4Client => {
        //process data received from server
        const { message } = data4Client;
        logger.info("Server says: " + message);

        // destroy socket after server responds
        destroySocket(socketClient);

        // return data for testing
        resolve(data4Client);
      });

      // if response takes longer than 5 seconds throw error
      setTimeout(() => {
        reject(new Error("Failed to get reponse, connection timed out..."));
      }, 5000);
    });

    // define data 4 server
    const data4Server = { message: "CLIENT ECHO" };

    // emit event with data to server
    logger.info("Emitting ECHO event");
    socketClient.emit(ev.com_ECHO, data4Server);

    // wait for server to respond
    const { status, message } = await serverResponse;

    // check the response data
    expect(status).toBe(200);
    expect(message).toBe("SERVER ECHO");
  });

  test("test BELLO", async () => {
    const socketClient = await initSocket();
    const serverResponse = new Promise((resolve, reject) => {
      socketClient.on(ev.res_BELLO, data4Client => {
        const { message } = data4Client;
        logger.info("Server says: " + message);
        destroySocket(socketClient);
        resolve(data4Client);
      });

      setTimeout(() => {
        reject(new Error("Failed to get reponse, connection timed out..."));
      }, 5000);
    });

    const data4Server = { message: "CLIENT BELLO" };
    logger.info("Emitting BELLO event");
    socketClient.emit(ev.com_BELLO, data4Server);

    const { status, message } = await serverResponse;
    expect(status).toBe(200);
    expect(message).toBe("SERVER BELLO");
  });
});

----脚注----

根据您设置服务器环境的方式,您可能会遇到同时从同一个项目运行的 socket.io 和 socket.io-client 之间的环境冲突。在这种情况下,最好将项目分成“测试客户端”和服务器。如果您遇到此问题,请查看以下 repo。

https://github.com/PatMan10/testing_socketIO_server_v2

【讨论】:

    【解决方案5】:

    在 OP 的代码中,

    socket.on('connect', function(done) {
        console.log('worked...');
        done();
    });
    

    done 应用于错误的回调。它应该从 socket.on 回调中移除并添加到 Mocha 的 it 块回调中:

    it('First (hopefully useful) test', function (done) {
      var socket = io.connect('http://localhost:3001');
      socket.on('connect', function () {
        console.log('worked...');
        done();
      });
    });
    

    一个完整的例子

    现有答案很好,但没有显示最终正在测试的服务器。这是带有console.logs 的完整版本,用于说明正在发生的事情。解释如下。

    src/server.js:

    const express = require("express");
    
    const createServer = (port=3000) => {
      const app = express();
      const http = require("http").Server(app);
      const io = require("socket.io")(http);
      
      io.on("connection", socket => {
        console.log("[server] user connected");
        
        socket.on("message", msg => {
          console.log(`[server] received '${msg}'`);
          socket.emit("message", msg);
        });
        socket.on("disconnect", () => {
          console.log("[server] user disconnected");
        });
      });
      
      http.listen(port, () =>
        console.log(`[server] listening on port ${port}`)
      );
      return {
        close: () => http.close(() => 
          console.log("[server] closed")
        )
      };
    };
    module.exports = {createServer};
    

    test/server.test.js:

    const {expect} = require("chai");
    const io = require("socket.io-client");
    const {createServer} = require("../src/server");
    const socketUrl = "http://localhost:3000";
    
    describe("server", function () {
      this.timeout(3000);
      
      let server;
      let sockets;
      beforeEach(() => {
        sockets = [];
        server = createServer();
      });
      afterEach(() => {
        sockets.forEach(e => e.disconnect())
        server.close();
      });
      
      const makeSocket = (id=0) => {
        const socket = io.connect(socketUrl, {
          "reconnection delay": 0,
          "reopen delay": 0,
          "force new connection": true,
          transports: ["websocket"],
        });
        socket.on("connect", () => {
          console.log(`[client ${id}] connected`);
        });
        socket.on("disconnect", () => {
          console.log(`[client ${id}] disconnected`);
        });
        sockets.push(socket);
        return socket;
      };
      
      it("should echo a message to a client", done => {
        const socket = makeSocket();
        socket.emit("message", "hello world");
        socket.on("message", msg => {
          console.log(`[client] received '${msg}'`);
          expect(msg).to.equal("hello world");
          done();
        });
      });
      
      it("should echo messages to multiple clients", () => {
        const sockets = [...Array(5)].map((_, i) => makeSocket(i));
        
        return Promise.all(sockets.map((socket, id) =>
          new Promise((resolve, reject) => {
            const msgs = [..."abcd"].map(e => e + id);
            msgs.slice().forEach(e => socket.emit("message", e));
          
            socket.on("message", msg => {
              console.log(`[client ${id}] received '${msg}'`);
              expect(msg).to.equal(msgs.shift());
              
              if (msgs.length === 0) {
                resolve();
              }
            });
          })
        ));
      });
    });
    

    总之,服务器导出一个函数,允许从头开始创建服务器应用程序,允许每个 it 块是幂等的,并避免服务器状态在测试之间传递(假设服务器上没有持久性)。创建应用程序会返回一个带有close 函数的对象。 socket.disconnect() 必须在每个测试中为每个套接字调用以避免超时。

    鉴于这些要求,测试套件遵循以下每次测试设置/拆卸工作流程:

    let server;
    let sockets;
    beforeEach(() => {
      sockets = [];
      server = createServer();
    });
    afterEach(() => {
      sockets.forEach(e => e.disconnect())
      server.close();
    });
    

    makeSocket 是一个可选的帮助器,用于减少连接和断开套接字客户端的重复样板。它确实会对sockets 数组产生副作用,以便稍后进行清理,但这是从it 块的角度来看的实现细节。测试块不应触及serversockets 变量,尽管其他工作流程可能取决于需要。关键要点是测试用例幂等性并在每个测试用例后关闭所有连接。

    客户端上socket.connect 对象上的选项可让您选择套接字的传输和行为。 "force new connection": true 为每个套接字创建一个新的Manager,而不是重用现有的,transports: ["websocket"] 立即从长轮询升级到 WS 协议。

    使用it("should ... ", done => { /* tests */ }); 并在回调中完成所有工作后调用done() 或返回一个promise(并省略it 回调的done 参数)。上面的示例显示了这两种方法。


    在这篇文章中使用:

    • node: 12.19.0
    • chai:4.2.0
    • express:4.16.4
    • mocha:5.2.0
    • socket.io:2.2.0
    • socket.io-client:2.2.0

    【讨论】:

      【解决方案6】:

      我遇到了这个问题:如果您不知道服务器需要多长时间响应,如何使用“socket.io-client”进行单元测试?

      我已经用 mochachai 解决了:

      var os = require('os');
      var should = require("chai").should();
      var socketio_client = require('socket.io-client');
      
      var end_point = 'http://' + os.hostname() + ':8081';
      var opts = {forceNew: true};
      
      describe("async test with socket.io", function () {
      this.timeout(10000);
      
      it('Response should be an object', function (done) {
          setTimeout(function () {
              var socket_client = socketio_client(end_point, opts);  
      
              socket_client.emit('event', 'ABCDEF');
      
              socket_client.on('event response', function (data) {
                  data.should.be.an('object');
                  socket_client.disconnect();
                  done();
              });
      
              socket_client.on('event response error', function (data) {
                  console.error(data);
                  socket_client.disconnect();
                  done();
                  });
              }, 4000);
          });
      });
      

      【讨论】:

      • 谢谢你。 { forceNew: true } 在这里非常重要:)
      猜你喜欢
      • 2011-03-28
      • 2011-12-27
      • 1970-01-01
      • 1970-01-01
      • 2018-02-06
      • 2021-05-21
      • 1970-01-01
      • 2017-11-23
      • 1970-01-01
      相关资源
      最近更新 更多