71

任何人都可以使用 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)

有什么建议么?

4

6 回答 6

72

经过进一步的戳戳,我发现了一些非常有用的信息。在作者的示例中,他指出了在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()中的位置对于建立连接至关重要。例如,如果您在侦听器中注释掉,然后将其添加到一个范围之外(就在退出之前),您将看到“no connection to break...”消息而不是“disconnecting...”消息。像这样:beforeEachsocket.on('connect'...)done()beforeEach

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)
于 2013-03-21T16:30:48.640 回答
12

在此处提供已接受答案的扩展。具有基本的客户端到服务器通信,可用作其他未来测试的样板。使用 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;
    });
  });

});
于 2016-05-30T04:32:52.110 回答
6

自己处理回调和 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)
    })
})
于 2021-02-26T07:25:14.413 回答
4

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

查看 GitHub 上的 repo 以获取完整的源代码。

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

于 2019-01-27T16:01:05.260 回答
3

In OP's code,

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

the done was applied to the wrong callback. It should be removed from the socket.on callback and added to Mocha's it block callback:

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

A complete example

Existing answers are great but don't show the server ultimately being tested. Here's a complete version with console.logs to illustrate what's going on. Explanation follows.

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

In summary, the server exports a function that lets a server app be created from scratch, allowing each it block to be idempotent and avoid server state from carrying between tests (assuming no persistence on the server otherwise). Creating an app returns an object with a close function. socket.disconnect() must be called per socket in each test to avoid timeouts.

Given these requirements, the testing suite follows this per-test setup/teardown workflow:

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

makeSocket is an optional helper to reduce the repeated boilerplate of connecting and disconnecting a socket client. It does produce a side effect on the sockets array for cleanup later, but this is an implementation detail from the it block's perspective. Test blocks shoudn't touch server or sockets variables, although other workflows are likely depending on need. The critical takeaways are test case idempotency and closing all connections after each test case.

Options on the socket.connect object on the client let you choose transport and behavior of the socket. "force new connection": true creates a new Manager per socket instead of reusing an existing one and transports: ["websocket"] upgrades to WS protocol from long polling immediately.

Use it("should ... ", done => { /* tests */ }); and invoke done() after all work is completed in callbacks or return a promise (and omit the done parameter to the it callback). The example above shows both approaches.


Used in this post:

  • 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
于 2020-10-22T15:51:07.840 回答
2

我遇到了这个问题:如果您不知道服务器需要多长时间响应,如何使用“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);
    });
});
于 2016-02-25T09:00:40.477 回答