Home Socket.io integration tests with chai and mocha
Post
Cancel

Socket.io integration tests with chai and mocha

Ah, all the goodies that you are building need to be of highest quality. But how is quality possible when no tests are in place?

If you have followed along, this very website promotes test driven development, very passionately if you ask me. I want my code to be thoroughly tested, I want confidence, I want speed. Remember, test driven development is the practice that makes you agile, in terms of speedy development and ability to cope with the code's evolution. You go faster when you write tests.

That is no different when we code real time applications, in this particular instance, with socket.io, which heavily relies on an event driven style. We need to make sure that our channels, our business rules and the wiring work properly and that is exactly the story that I am going to tell you, so let's dive in.

Prologue

What the heck is socket.io?

Socket.io is a JavaScript framework which builds on WebSockets technology.
From MDN, here's a quote for WebSockets:

WebSocketsis an advanced technology that makes it possible to open an interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply.

But why we bother using socket.io? The answer lies to browser compatibility. Yeah, old story, not all browsers support this kind of technology, so that's where socket.io comes into play.
Using this one you can eliminate issues on WebSocket connections. It is a simplified wrapper which creates and works with WebSockets. An abstraction layer upon them, like the jQuery of WebSockets, if you like.
And problems like disconnections, timeouts, heartbeats, old browser, long polling, they are gone, it takes care of them for us.

So, if I were to describe it, I would say that socket.io is an event-based framework, which allows bidirectional communication.
In simplified terms, my latter statement can be translated like: Users can connect to a socket and can send and receive data to connected sockets in real time.
Also, this framework loves the Pub/Sub pattern, so if you are familiar with it, you won't have any issue, you will get used to it extremely fast.

By the way, socket.io has browser support for smartphone, desktop and tablet and we can say that almost 90% of browsers can use that without any problems at all.

A'right and what about Mocha?

That one is easy. It is a testing framework running on Node.js, which plays very well with asynchronous tests. The bread and butter of this very story is asynchronous tests, because it is the only way to test socket.io.
So, your Node.js backend tests should be supported by Mocha.

Lastly, some short comments on chai assertion library. It will help you with assertions, it can work with the testing framework of your preference and also it can accommodate your testing style, meaning, you can test in BDD or TDD style, based on the assertion interface you will chose. I chose a TDD style and will work with chai.assert.

Tests will save you

Why we need tests?

That should be obvious, right?
No code should ship to production without its pairing tests. Period.

We need to test the business rules and the behavior that we are going to build, we need to know what happens when we fire an event from the browser, how the socket.io server acts on that.
We need to know how the client code is acting on socket server events.
We need to be able to refactor the code and be certain that it will work, test is going to let us know if we did break something.

What you should take away from this section is that tests are important, so do write them. Please. Por favor.

What type of tests are we going to write

Hmm, let's think about for a moment.
We have two parts interacting, we have socket.io server and the socket.io client. The first is running on the backend and the latter is running on browser, so we need to connect these two different worlds together. Obviously we are not testing a unit.
We need to setup a server and a client on each test. The client will publish an event to the server and if the server knows how to respond to that event, subscribers will catch that and execute a callback.
It should be obvious by now, that we are going to write integration tests, as they cover less depth (less depth means more paths and more classes/units to assert), we are far away from a single unit of code, we test multiple.

Notice that I have decorated in bold and italics the word 'callback'. This is tricky, because a callback is not handled by the code we are writing, thus the test, it is handled by another entity, that fires that callback when its work is done. That means, our test is going to linearly execute and then exit, as that kind of execution that was described is asynchronous. The callback is not going to fire by that time, so the test is going to lie to us (no assertions at the end, assertions are only in the callback body, so the test will exit with a success code).

Thankfully, Jasmine( I love this framework <3 ) provides us with asynchronous support.
Yourit, beforeEach, etc. methods can take an optional argument which signals Jasmine that the asynchronous operation has ended, thus the test (or the setup methods) can terminate.
By default it awaits for 5 seconds before it nukes your test. When your asynchronous code is over, you just call this argument like a method and you are done. If you want to fail and complete the spec, you call its fail method.

Examples

This is a successful operation

This is an operation which you want to fail for some reason, but deliberately, not because of timeout

Please note that if you do not provide the doneargument, the test(s) above will fail either way, with a timeout reason.

You show me yours, I'll show you mine

Enough with the talking, it's time to look more on the code and get our hands dirty.

Packages

For this one, at minimum we need:

  • Jasmine
  • Mocha
  • Chai
  • socket.io
  • socket.io-client

package.json

Take a look at my package.json file for reference (for brevity, I have omitted some code).
I use Jasmine 2 and socket.io 2
Of course, because I use Typescript, I have downloaded the appropriate types as well

Configuring mocha

Looking at the package.json above, the command for local testing is test:backend:dev, which in turn uses Gulp to configure mocha and fire the test runner.
Let's take a look.

I use gulp-mocha module here, which is only a wrapper around mocha, but helps me greatly to build this task.
So, I create a function, in order to reuse it in mocha:dev and mocha:travis tasks. Let's focus on the mocha:dev task, which runs the tests locally.
What I do here, is to run the Typescript compiler before the task executes (the ["ts"] dependency), then watch all the Typescript files under a certain directory and when a change happens, run the Typescript compiler and mocha in order.

Setting up tests, configuring socket.io

Next step is to configure tests to create a socket.io server and a client. After that, we can proceed to tests.
Following code shows a basic setup. I import the socket.io server and socket.io-client, I create some configuration for the client and then I instantiate them, first the server which listens to a specific port (5000 in this case) and then the client which connects to the server with the aforementioned options.

Let me explain some parts of it:

  • Line 13: I choose to use the assert style from chai (I like more a TDD approach on this).
    Should be used likeassert.typeOf(foo, 'string');
  • Line 14-18: I set the socketUrl to be on localhost and listen to port 5000. This is the same port I am going to provide to the socket server to listen to.
    The options are meant for the socket.io-client, they are pretty much standard and self-explanatory.
  • Line 27: Now, in beforeEach() method I create a new instance of the socket.io server and the client. In line 27 I create a new server which listens to port 5000.
  • Line 28-29: I have a custom class named Socket, which receives an io server, so I pass it there and call the connect() method of that class, which only registers events using the .on() function of the socket.io server (more on this later).
  • Line 30: I create a new socket.io client which listens to the socketUrl and takes the options I defined earlier.
  • Line 33-36: In afterEach() function, which is called after each spec is completed, I close the connection of server and the client. I do this in order to not bleed invocations to listeners which may be set on other tests, so I make sure I test in isolation.

Action baby

Time for some tests. Before I do that, as promised, let's check on the Socket class and some of its code.

When code calls the connect() method, events are subscribed in the socket.io server, like the RoomCreateEvent. When the client code emits that event, server will invoke this callback.
So, looking at the RoomCreateEvent, we can see that it receives some kind of data, of type CreateRoomEventArgs and a callback, which callback returns a boolean and an optional string.

Now let's focus on the tests. First, let's write a test to make sure it can connect to the socket.

First thing I do is to subscribe to the "connect" event for the client, so as soon as the client is connected I can proceed with my tests, like setting or emitting other events, and so on and so forth. So, the client connects, I assert the client.connected property to make sure he is connected and then I call the done(); method to terminate the test.
Notice that I know this test is valid because it terminates successfully and all because of the done argument. If I hadn't call this method the test would fail with a timeout reason.

Good, let's proceed in more advanced techniques now. I want to test the functionality on RoomCreateEvent event. Let's see some examples

As you can see, asynchronous tests are not following the norm of normal tests, you declare your callback first, but you have your assertion logic there, I know, it looks weird.
So, this test case wants to test if the server will respond with an InternalServerErrorEvent if I pass undefined input data.

So, as usual, I subscribe to the "connect" event. In its callback now, first I subscribe to the InternalServerErrorEvent, which is the event that I expect the server to respond with. My assertions are all there, in its callback, I assert the exception object that is returned and I terminate the test, calling the done() method.

In order for the InternalServerErrorEvent callback to fire, I need to emit it somehow.
Fortunately, my server emits that (if you check at line 25 of Socket class code, you will see the following linewhich indeed emits an event to whoever is subscribing to it.
this.emitInternalServerError(socket, error);

Notice that I assert the return value of the RoomCreateEvent callback. That is perfectly legal, if you check at line 24of Socket class, you will notice that I indeed call the callback and pass an object with a false value on access property
callback({ access: false });
But my test will not terminate there, it will terminate at InternalServerErrorEvent callback.

Let's look at one more test.
In the following, I emit the RoomCreateEvent with some valid data. My expectation is to:

  • Receive an object with access property set to true and roomId property set to the roomId that was generated.
  • Publish 3 events, which I expect to inform me that one user is listed into that room, I have 1 active room in total and 1 active user in total.
    • RoomShowAllEvent. Returns the number of users connected to that room.
    • RoomsAllEvent. Returns the total number of active rooms.
    • UsersAllEvent. Returns the total number of users in all rooms.

In test, as I mentioned earlier, I first set my subscribers then I publish the event I am looking for. I publish the event with the emit method at the bottom of the test.

First I subscribe to the RoomShowAllEvent.
Then, I subscribe to RoomsAllEvent.
Finally, I subscribe to UsersAllEvent. This one terminates the test.

I did this on purpose, because this is the order they are executed in the createRoom method from Socket class. Let me refresh your memory with a snippet:

I just followed the normal flow. This can make the reader understand better my intention, but it is also practical. The last event that is fired is the UsersAllEvent, so it makes sense to terminate the test when it is fired. If I called the done() in let's say RoomShowAllEvent event, then the RoomsAllEvent and UsersAllEvent callbacks wouldn't fire and as a result their assertions would not assert. Just make sure you end the test on the last callback that will run, find your exit point. :)

For more advanced techniques and tests, like having multiple clients connect and how they would interact, take a look on my Github repository, you will find over 2000 lines of code and 92 tests only for this socket.io implementation.

Summary

Use Mocha for your Node.js tests and make sure you support asynchronous testing.
From that point on, if you get familiar with the socket.io API, tests won't be that hard to author, you just need to emit client events to the server and assert server's callback. You might also need to register to client events also, if your server emits events that the client should listen to.

Don't forget to call the done optional argument after your last callback, so you can mark your tests as successful, else you will have timeout issues and of course test failures.

If you want to see more, or get some ideas, please feel free to take a look at my repository, here, you can find tests on socket.io server and other useful stuff. Disclaimer, the work is in progress on that one.

This post is licensed under CC BY 4.0 by the author.

Migrating from typings to npm @types

ES6 generators and async/await in Typescript

Comments powered by Disqus.