Socket.io is awesome, but testing it can be a pain in the ass. It requires a whole bunch of set up, some crucuial tear down. When working with multiple client connections, which is pretty much the whole point of Socket.io, you'll need to listening to a cascading list of 'connect' events just to make a simple test. Throw in some setTimeout
to check if/how many times something was called, and you have a nasty looking test file.
To deal with all this, I put together socket-tester
that takes care of the client set up for you and throws in some helper functions mimicking some simple spy functions. Check out the repo here. Likewise, it's available on npm
:
npm install socket-tester
In this post, I'll outline the standard way to test Socket.io code using Mocha and Chai, and then how to do it using socket-tester
.
The Problem
I'll skip the Express server setup and jump right into the Socket.io relevant code. Check out the repo for the full code base.
The code we'll be testing is a brutally simple Socket.io setup. It allows users to join rooms and send messages to other users in those rooms. Groundbreaking, I know.
// socket/socket.config.js
/*jslint node: true */
'use strict';
module.exports = function(io){
io.on('connection', function(socket){
socket.on('join room', function(roomname){
socket.roomname = roomname;
socket.join(roomname);
});
socket.on('message', function(msg){
io.to(socket.roomname).emit('message', msg);
});
});
};
I'll go through how to set up three tests for some common use cases without using socket-tester
. If you're interested in going a bit more in depth, this post really helped me get started.
To run the tests, make sure you have mocha
installed globally. I'm going to use chai
as my assertion library and socket.io-client
to handle setting up my connections.
npm install mocha -g
npm install chai socket.io-client
Next, create a test
directory and test.js
to hold onto all our tests. Make sure to include a reference to your server file so it's up and running when you start your tests! I required and saved the server file as app
here.
The basic setup:
// test/test.js
var expect = require('chai').expect;
var io = require('socket.io-client');
var app = require('../testServer/index');
var socketUrl = 'http://localhost:3000';
var options = {
transports: ['websocket'],
'force new connection': true
};
var room = 'lobby';
describe('Sockets', function () {
var client1, client2, client3;
// testing goodness goes here
});
Then while in the project directory run:
mocha -R spec
There are four main steps in testing Socket.io code. First, set up the connections using socket.io-client
. Second, set up all even listeners. Third, trigger all the emit events we need to look at. And last, disconnect all the client connections.
There are of course a few gotchas that come with the territory. When dealing with multiple clients, later connection events must be done inside of the connection
event callback of earlier connections. Likewise, emit
events can only be reliably called when all clients are connected. This can lead to some cascading listener callbacks when you begin testing multiple clients.
Here's a simple example testing the users can message each other. Notice the actual test is held inside client1 on 'message'
listener.
// ... test.js
it('should send and receive a message', function (done) {
// Set up client1 connection
client1 = io.connect(socketUrl, options);
// Set up event listener. This is the actual test we're running
client1.on('message', function(msg){
expect(msg).to.equal('test');
// Disconnect both client connections
client1.disconnect();
client2.disconnect();
done();
});
client1.on('connect', function(){
client1.emit('join room', room);
// Set up client2 connection
client2 = io.connect(socketUrl, options);
client2.on('connect', function(){
// Emit event when all clients are connected.
client2.emit('join room', room);
client2.emit('message', 'test');
});
});
});
Not too bad, but a lot of setup and some finicky tear down. This next test gets even hairier. I want to test if users in other rooms will hear a message. client1
and client2
will be in the same room, and client3
will be a different one. So only client2
should hear client1
send message. To track this, I'll use a counter variable to see how many times the event listeners for client2 and 3 were called. Because we're testing for a function that is never supposed to be called, we'll have to run our tests inside of a setTimeout
function to wait for all the emit
events to finish.
it('should send and receive a message only to users in the same room', function (done) {
client2CallCount = 0;
client3CallCount = 0;
client1 = io.connect(socketUrl, options);
client1.on('connect', function(){
client1.emit('join room', room);
client2 = io.connect(socketUrl, options);
client2.emit('join room', room);
client2.on('connect', function(){
client3 = io.connect(socketUrl, options);
client3.emit('join room', 'test');
client3.on('connect', function(){
client1.emit('message', 'test');
});
client3.on('message', function(){
client3CallCount++;
});
});
client2.on('message', function(){
client2CallCount++;
});
});
setTimeout(function(){
expect(client2CallCount).to.equal(1);
expect(client3CallCount).to.equal(0);
client1.disconnect();
client2.disconnect();
client3.disconnect();
done();
}, 25);
});
This was about the point I got to when I thought there must be a better way.
Socket-Tester
I put together socket-tester
to clean up a lot of the repeated code. It will take care of all the set up and connection mumbly jumbly, as well as automatically cleaning up the connections for you. It also adds in some nice helper functions to test common cases such as an event listener that shouldn't be called, or an event listener that should be called n times.
The socket-tester
module exposes a constructor function which requires a socket.io-client
connection, socketUrl
, and socketOptions
parameters. Here's the basic setup.
var expect = require('chai').expect;
var io = require('socket.io-client');
var app = require('../testServer/index');
var SocketTester = require('../index');
var socketUrl = 'http://localhost:3000';
var options = {
transports: ['websocket'],
'force new connection': true
};
var socketTester = new SocketTester(io, socketUrl, options);
var room = 'lobby';
describe('Sockets', function () {
});
The syntax is based around socketTester.run()
which takes two arguments. An array of client objects, and Mocha's done
function.
socketTester.run([client1, client2, client3], done);
A client object is setup as follows:
var client = {
on: {
'event name': <event handler function>
},
emit: {
'event name': <event emit value>
}
};
Here are the same tests as above using socket-tester
.
// test.js
it('should send and receive a message', function(done){
var client1 = {
on: {
'message': socketTester.shouldBeCalledWith('test')
},
emit: {
'join room': room
}
};
var client2 = {
emit: {
'join room': room,
'message': 'test'
}
};
socketTester.run([client1, client2], done);
});
it('should send and recieve a message only to users in the same room', function (done) {
var client1 = {
on: {
'message': socketTester.shouldBeCalledNTimes(1)
},
emit: {
'join room': room,
}
};
var client2 = {
emit: {
'join room': room,
'message': 'test'
}
};
var client3 = {
on: {
'message': socketTester.shouldNotBeCalled()
},
emit: {
'join room': 'room'
}
};
socketTester.run([client1, client2, client3], done);
});
I like this approach much better. It has a flatter structure regardless of how many clients you want to test. The helper functions also make it much easier to check the most common use cases for Socket.io.
For more info check out the docs. I'd love any feedback, let me know what you think!