71

WebSocket(Socket.io)を使用してNode.jsの堅実で非常に単純な単体テストを提供できる人はいますか?

Node.jsにsocket.ioを使用しており、テストでサーバーへのクライアント接続を確立するために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()beforeEachあることがわかりました。socket.on('connect'...)たとえば、リスナーでコメントアウトしてからdone()、スコープを1つ追加すると(を終了する直前beforeEach)、「disconnecting...」メッセージの代わりに「noconnectiontobreak...」メッセージが表示されます。そのようです:

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

ここで受け入れられた答えの拡張を提供します。他の将来のテストの定型文として役立つ基本的なクライアント/サーバー通信があります。モカ、チャイ、そして期待を使用します。

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

コールバックと約束を自分で処理することは困難である可能性があり、重要な例はすぐに非常に複雑になり、読みにくくなります。

NPMを介して利用可能なsocket.io-await-testというツールがあり、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のリポジトリを確認してください。

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の間で環境の競合が発生する場合があります。その場合、プロジェクトを「テストクライアント」とサーバーに分ける方がよいでしょう。この問題が発生した場合は、以下のレポをチェックアウトしてください。

https://github.com/PatMan10/testing_socketIO_server_v2

于 2019-01-27T16:01:05.260 に答える
3

OPのコードでは、

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

done間違ったコールバックに適用されました。コールバックから削除して、socket.onMochaのitブロックコールバックに追加する必要があります。

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

完全な例

既存の回答は素晴らしいですが、最終的にテストされているサーバーを示していません。console.logこれは、何が起こっているのかを説明するためのs付きの完全なバージョンです。説明は次のとおりです。

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ブロックをべき等にし、サーバーの状態がテスト間で実行されないようにします(サーバーに永続性がないことを前提としています)。アプリを作成すると、関数を含むオブジェクトが返されますclosesocket.disconnect()タイムアウトを回避するために、各テストでソケットごとに呼び出す必要があります。

これらの要件を前提として、テストスイートは次のテストごとのセットアップ/ティアダウンワークフローに従います。

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

makeSocketは、ソケットクライアントの接続と切断の繰り返しの定型文を減らすためのオプションのヘルパーです。sockets後でクリーンアップするためにアレイに副作用が発生しますが、これはitブロックの観点からの実装の詳細です。他のワークフローは必要に応じて可能性がありますが、テストブロックは触れserverたり変数を使用したりしないでください。sockets重要なポイントは、テストケースのべき等性と、各テストケースの後にすべての接続を閉じることです。

クライアント上のオブジェクトのオプションを使用するsocket.connectと、ソケットのトランスポートと動作を選択できます。既存のソケットを再利用する代わりに、ソケットごと"force new connection": trueに新しいものを作成し、長いポーリングからすぐにWSプロトコルにアップグレードします。Managertransports: ["websocket"]

コールバックですべての作業が完了した後に使用it("should ... ", done => { /* tests */ });して呼び出すdone()か、promiseを返します(そしてコールバックdoneへのパラメーターを省略しitます)。上記の例は、両方のアプローチを示しています。


この投稿で使用:

  • 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」で単体テストを行う方法は?

私はモカチャイを使って解決しました:

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 に答える