Unit Testing Socket.IO with Jasmine

Unit Testing Socket.IO with JasmineRecently I finished up talking about how to use Jasmine as your JavaScript Unit Testing framework (part 1 and part 2). In there I mentioned how to test asynchronous methods, but I decided to write up a more useful example while giving you a sneak peak of the current state of a component from my Minecraft Server Manager project. Here you’ll see how I handled Socket.IO both for the application and for testing.

The Socket Utility

I love the Socket.IO library because it is so simple to use. It didn’t quite have everything I wanted, though. Specifically, I wanted the ability to register a handler to fire on “connect”, but if it was already connected, I wanted it to fire the handler immediately. Also, I wanted to be able to inform the entire application when it was (dis)connected through the application event hub. For these reasons, and because I didn’t want my components dealing directly with Socket.IO (just in case I decided to switch libraries later), I decided to create a Socket.IO wrapper object. You can see the entire JavaScript file below:

define(
["io", "underscore"],
function(io, _) {

    Socket = function(options) {
        var settings = {
            port: "8080",
            "auto connect": false
        };

        if (typeof options.io === "object") {
            _.extend(settings, options.io);
        }

        this.vent = options.vent;
        this.socket = io.connect(":" + settings.port, settings).socket;
        
        this._listenTo(this.socket, {
            "connect": this.onConnect,
            "disconnect": this.onDisconnect
        });
    };

    _.extend(Socket.prototype, {
        isConnected: function() {
            return this.socket.connected;
        },

        on: function(event, handler, context) {
            this.socket.on(event, _.bind(handler, context));

            if (event === "connect" && this.isConnected()) {
                handler.call(context);
            }
        },

        emit: function() {
            this.socket.emit.apply(this.socket, arguments);
        },

        connect: function() {
            this.socket.connect();
        },

        disconnect: function() {
            this.socket.disconnect();
        },

        onConnect: function() {
            this.vent.trigger("status:connected");
        },

        onDisconnect: function() {
            this.vent.trigger("status:disconnected");
        },

        _listenTo:function(obj, bindings) {
            var self = this;

            _.each(bindings, function(callback, event) {
                obj.on(event, _.bind(callback, self));
            });
        }
    });

    return Socket;
});

One of the big things to notice is the constructor. First of all, I take in a bunch of options. If you know anything about Backbone.Marionette’s Application initializers, you’ll know that these options are passed around to everything that is created in the initializers. All you really need to know is that these are global configuration options that pretty much everything in the app knows about.

Within these options is a bit about Socket.IO in the io property. These options are used for connecting to the server correctly. I also have some default settings and I let the options argument override these settings. You’ll notice that I have the default option for ‘auto connect’ set to false. This allows me to create a new Socket() without it necessarily connecting before I need it to.

The other option that I care about is vent, which is the event hub. I’ve talked about passing this around before when I talked about Dependency Injection. Then, in the constructor, I use my little utility function to bind to the “connect” and “disconnect” events on the socket so that I can use the event hub to alert the rest of the app of the state of the connection.

The rest of Socket is pretty much just wrapper functions, except on, which, as I described earlier, will immediately execute a “connect” handler if the socket is already connected.

Testing the Socket Wrapper

Because I made this wrapper, I actually had to test it. Normally, there is no reason to actually test third party libraries, unless they weren’t properly tested already. However, you do need to test the functionality of your own code, and you should be sure that the third party library is properly integrated into your system.

Here’s my spec for my Socket wrapper:

define(
["utils/socket", "backbone"],
function(Socket, Backbone) {

/* SETUP */
    var Vent = function(){};
    _.extend(Vent.prototype, Backbone.Events);

    var options = {
        io: {
            port: "8080",
            "force new connection": true
        }
    };
/* END SETUP */

/* TESTS */
    describe("Socket Utility", function() {
        beforeEach(function(){
            this.vent = new Vent();
            spyOn(this.vent, "on").andCallThrough();
            spyOn(this.vent, "trigger").andCallThrough();

            options.vent = this.vent;

            this.appSocket = new Socket(options);
        });

        afterEach(function() {
            this.appSocket.socket.disconnectSync();
        });

        it("is initialized", function(){
            expect(this.appSocket).not.toBeNull();
            expect(this.appSocket.vent).not.toBeNull();
            expect(this.appSocket.socket).not.toBeNull();
            expect(this.appSocket.socket.$events.connect).toBeTruthy();
            expect(this.appSocket.socket.$events.disconnect).toBeTruthy();
        });

        describe("#connect", function() {
            it("connects socket to Socket.IO server", function() {
                runs(function(){
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    // Trust `isConnected` rather than checking Socket.IO's implementation
                    // because if `isConnected` doesn't work, it'll show up in those tests
                    // This is also the condition for the test to pass, so no `expect`
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);
            });
        });

        describe("#disconnect", function() {
            it("disconnects socket from server", function() {
                runs(function() {
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    // Trust `isConnected` rather than checking Socket.IO's implementation
                    // because if `isConnected` doesn't work, it'll show up in those tests
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);

                runs(function() {
                    this.appSocket.disconnect();
                });

                waitsFor(function(){
                    // Trust `isConnected` rather than checking Socket.IO's implementation
                    // because if `isConnected` doesn't work, it'll show up in those tests
                    // This is also the condition for the test to pass, so no `expect`
                    return !this.appSocket.isConnected();
                }, "The socket should disconnect", 1500);
            });
        });

        describe("#isConnected", function() {
            it("tells us we're disconnected before we connect", function() {
                expect(this.appSocket.isConnected()).toBeFalsy();
            });

            it("tells us we're connected after we connect", function() {
                runs(function() {
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    // Look for internal implementation of `isConnected` since we're
                    // testing to make sure `isConnected` matches it
                    return this.appSocket.socket.connected;
                }, "The socket should connect", 1500);

                runs(function() {
                    expect(this.appSocket.isConnected()).toBeTruthy();
                });
            });

            it("tells us we're disconnected after we disconnect", function() {
                runs(function() {
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    // Look for internal implementation of `isConnected` since we're
                    // testing to make sure `isConnected` matches it
                    return this.appSocket.socket.connected;
                }, "The socket should connect", 1500);

                runs(function() {
                    this.appSocket.disconnect();
                });

                waitsFor(function(){
                    // Look for internal implementation of `isConnected` since we're
                    // testing to make sure `isConnected` matches it
                    return !this.appSocket.socket.connected;
                }, "The socket should disconnect", 1500);

                runs(function() {
                    expect(this.appSocket.isConnected()).toBeFalsy();
                });
            });
        });

        describe("#on", function() {
            var mock;
            
            beforeEach(function() {
                mock = {
                    testFunc: function(){}
                };
                spyOn(mock, "testFunc");
            });

            it("adds events to the IO Socket", function() {
                this.appSocket.on("event", mock.testFunc, mock);

                expect(this.appSocket.socket.$events.event).not.toBeNull();
                expect(this.appSocket.socket.$events.event).not.toBeUndefined();
            });

            it("will call 'connect' event handlers when the socket connects", function() {
                runs(function() {
                    this.appSocket.on("connect", mock.testFunc, mock);
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);

                runs(function() {
                    expect(mock.testFunc).wasCalled();
                });
            });

            it("will call 'connect' handler immediately when added if the socket is already connected", function() {
                runs(function() {
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);

                runs(function() {
                    this.appSocket.on("connect", mock.testFunc, mock);
                    expect(mock.testFunc).wasCalled();
                });
            });

            it("will call 'disconnect' event handlers when the socket disconnects", function() {
                runs(function() {
                    this.appSocket.on("disconnect", mock.testFunc, mock);
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);

                runs(function() {
                    this.appSocket.disconnect();
                });

                waitsFor(function(){
                    return !this.appSocket.isConnected();
                }, "The socket should disconnect", 1500);

                runs(function() {
                    expect(mock.testFunc).wasCalled();
                });
            });
        });

        describe("#emit", function() {
            beforeEach(function() {
                spyOn(this.appSocket.socket, "emit").andCallThrough();
            });

            it("calls the real socket's emit with the same arguments", function() {
                this.appSocket.emit("event", "a test argument");

                expect(this.appSocket.socket.emit).wasCalledWith("event", "a test argument");
            });
        });

        describe("#onConnect", function() {

            it("is called when the socket connects and triggers 'status:connected' on the vent", function() {
                // We can't spy on onConnect because it is already assigned to run on 
                // 'connect' in the constructor, so the spy won't be run, the original will
                // be. So we just test to see if the effect of onConnect is carried out.
                runs(function() {
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);

                runs(function() {
                    expect(this.appSocket.vent.trigger).wasCalledWith("status:connected");
                });
            });
        });

        describe("#onDisconnect", function() {

            it("is called when the socket disconnects and triggers 'status:disconnected' on the vent", function() {
                // We can't spy on onDisconnect because it is already assigned to run on 
                // 'disconnect' in the constructor, so the spy won't be run, the original will
                // be. So we just test to see if the effect of onDisconnect is carried out.
                runs(function() {
                    this.appSocket.connect();
                });

                waitsFor(function(){
                    return this.appSocket.isConnected();
                }, "The socket should connect", 1500);

                runs(function() {
                    this.appSocket.disconnect();
                });

                waitsFor(function(){
                    return !this.appSocket.isConnected();
                }, "The socket should disconnect", 1500);

                runs(function() {
                    expect(this.appSocket.vent.trigger).wasCalledWith("status:disconnected");
                });
            });
        });
    });
/* END TESTS */

});

The first thing we do is a bit of setup. We create a mock for the event hub, which extends Backbone.Events so that we actually have the core functionality of the event hub. Then we put together the options. Notice the “force new connection” option. Normally, when you call io.connect(...) with the same URL as a previous call to it, it’ll return the same old socket that you had before. This is a problem because we want to be able to refresh which events are attached to the socket for each test. That’s where “force new connection” comes in. It forces Socket.IO to create a new socket each time so we don’t have duplicate event handlers registered.

Then we move on to the main describe block. Inside we create our setup and teardown activities with beforeEach and afterEach. In beforeEach we instantiate vent, spy on its main methods, and put it into the options. Then we create a new instance of our Socket. In afterEach we use Socket.IO’s synchronous method for disconnecting. We keep it synchronous because that makes it simpler.

I will only go over a few of the specs; you can look through the rest yourself if you want to. If you have any questions about a spec (especially one I didn’t go over), you can just leave a comment below the post.

In the specs that actually connect and/or disconnect from the server, I checked to see when it (dis)connected by calling my own appSocket.isConnected() method (as you can see on lines 51, 65, 76, 153, etc.) rather than querying the actual socket via appSocket.socket.connected. This is because I chose to trust that isConnected works unless the spec for that method told me otherwise. When I was testing isConnected (lines 81-127), I went to the actual socket to get my information (lines 94, 110, and 120).

If you look through the specs of isConnected, you’ll see how the asynchronous work is really done. As I described in my Jasmine tutorial, you call runs, waitsFor, runs. In the first runs call, you call the asynchronous method (connect or disconnect in this case). Then in waitsFor, you run the checks to detect if that operation finished. Finally, the second time you call runs, you can test to make sure the spec passed.

In the case of the spec starting at line 102, I need to disconnect, but in order to disconnect I need to connect first. So that is two asynchronous functions being run in the same spec. In these instances, you can continue to chain waitsFor and runs on to the end until you’ve completed all of your asynchronous tasks. So I connect, wait for it to finish connecting, then disconnect and wait for that to finish, and then test to see if the spec passes.

When I tested on (lines 129-198) you’ll notice that I didn’t actually test to see if Socket.IO would call the handlers after an event from the server came back. This is because I have no control over the server (with the exception of connecting and disconnecting, which I do test to make sure the handlers are called). This is also because I would be testing the third party library, rather than my own code, which I already said was unnecessary. In this case, I just made sure that the event handlers were properly attached to the true socket that I got from Socket.IO. You may also notice that the tests for emit, on lines 200-210, don’t actually test to see if anything was sent to the server. There are two reasons for this: 1) I didn’t connect to the server for that example, so I know nothing would have been sent and 2) my code doesn’t send anything to the server; Socket.IO does. Once again, I just need to make sure things are properly delegated to Socket.IO by using a spy to make sure that IO’s socket.emit was called correctly.

The High Timeout

My final point today is about the 1500 millisecond timeout I have set on the calls to waitsFor when I connect or disconnect. I originally had this set to 750, which worked flawlessly because I was only testing in Firefox. Later I began to test in Chrome and it was timing out. I had to double the time to wait in order for it not to time out in Chrome. I found this odd considering Chrome is touted as the faster browser. It seems like they may not have maximized their WebSocket performance though. I haven’t yet tested this thoroughly, but believe me when I say that I will. I will try to find this bottleneck, and if possible, I’ll find a way to work around it. In any case, expect me to report my findings in a future post. For now, I just wanted you to know that you may need to have some unusually high timeouts, even with local servers.

Conclusion

That’s about all the interesting stuff I could think to show you. I don’t claim to be an expert at this stuff. After all, I’ve only been unit testing for about a month and a half. But I did want to share what I know. If you see problems with some of the things in my tests and you know you’re right about it, go ahead and let me know.

Also, I wanted to let you all know that there really aren’t any reasons to unit test Socket.IO unless you are using some kind of wrapper like I did here, or if you’re testing a module that relies on Socket.IO in its methods, in which case it’d probably be better to mock your sockets if you can. If you’re testing to make sure things come back from the server correctly, that it integration testing, not unit testing, and should be handled in a different testing environment.

About the Author

Author: Joe Zim

Joe Zim

Joe Zimmerman has been doing web development ever since he found an HTML book on his dad's shelf when he was 12. Since then, JavaScript has grown in popularity and he has become passionate about it. He also loves to teach others though his blog and other popular blogs. When he's not writing code, he's spending time with his wife and children and leading them in God's Word.


One thought on “Unit Testing Socket.IO with Jasmine

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>