Building a Command-Line Chat App with Node.js and Socket.IO — Part 4

Teedus Chung
7 min readJun 18, 2021

--

The other parts of this project are as follows:

Goals

To develop any software, we need to have good practices. I mean we need to test our chat app. In particular, we need to test both the client-side and the server-side of the app independently for each implemented feature.

In this part, we’re going to implement different use cases to test if the server behaves in the right manner. Specifically, a list of operations needs to be tested as follows:

Testing server in the chat

  • Notify that a user joined the chat
  • Broadcast a message to others in the chat
  • List all users in the chat
  • A user quit the chat
  • Notify that a user quit the chat
  • Send a private message to a user

Testing server in a group

  • Notify that a user joined a group
  • Broadcast a message to a group
  • List all users that are inside a group
  • List the existing groups

Setting up

There are many test framework available, for example, Jest, Jasmine, Nightwatch, Karma, Cypress, etc. Here we’ll demonstrate how to test the app with Mocha. It’s a feature-rich JavaScript test framework running on Node.js and in the browser.

Navigate to the siochat directory then install Mocha as a development dependency for our project using npm as follows:

% npm install --save-dev mocha

Mocha automatically looks for our tests inside the test directory of our project. Hence, we’ll create this directory at the root of our project by:

% mkdir test

Then modify the test field in the package.json file to run tests using Mocha. Here is what it should look like:

/* package.json */

{
... "scripts": {
"test": "mocha"
},
...}

Now we can simply run the tests in the project using this simple command:

% npm test

Writing tests often requires using assertion library. Mocha does not discriminate whatever assertion library we choose to use. In the Node.js environment, we can use the built-in assert module as our assertion library. However, there are more extensive assertion libraries we can use, such as Chai, Unexpected, Expect.js, etc. For all the tests in this project, Chai will be used as the assertion library of choice.

Install Chai as a development dependency for our project as follows:

% npm install --save-dev chai

We can learn more about the assertions and assertion styles that Chai provides from this documentation.

Writing tests

BDD interface

Mocha provides a variety of interfaces, for instance, BDD, TDD and Exports for defining test suites, hook and individual tests. The default interface is BDD.

Here the BDD interface will be used for defining and writing the tests.

We can learn more about the available interfaces at here.

Now, let’s navigate to the test directory then create a test file named server.spec.js containing the following code:

/* test/server.spec.js */// Load the 'chai' module with the 'expect' style
const expect = require("chai").expect;
// Load the 'socket.io-client' module
const io = require("socket.io-client");
describe('----Testing server in the chat', function() { beforeEach(function (done) { //... }); afterEach(function (done) { //... }); it('Notify that a user joined the chat', function(done) { //... }); it('Broadcast a message to others in the chat', function(done) { //... }); //...}); // End describedescribe('----Testing server in a group', function() { beforeEach(function (done) { //... }); afterEach(function (done) { //... }); it('Notify that a user joined a group', function(done) { //... }); it('Broadcast a message to a group', function(done) { //... }); //...});

describe()

The describe() function is a way to group tests in Mocha. We can nest our tests in groups as deep as we deem necessary. describe() takes two arguments:

  1. A name of a test group
  2. A callback function

Hooks

Mocha makes provison for creating test hooks. Hooks basically are logic that have been configured to run before or after tests. They are useful for setting up preconditions for tests and cleaning resources after tests.

With the default BDD interface, we have four optional hooks:

  1. before() runs once before the first test in the block
  2. beforeEach() runs before each test in the block
  3. afterEach() runs after each test in the block
  4. after() runs once after the last test in the block

Hooks and tests will run together according to the following order:

before() -> beforeEach() -> it('1st test') -> afterEach()         -> beforeEach() -> it('2nd test') -> afterEach()        -> beforeEach() -> it('Last test') -> afterEach() -> after()

Like a describe() function, a hook also takes two arguments:

  1. A description that help us easily to track errors.
  2. A callback function that contains logic to be executed if the hook is triggered.

Testing server in the chat

beforeEach() and afterEach() test hook

Before executing each test, we’ll prepare two connected socket as socket_1 and sock_2. After each test is executed, we’ll disconnect both sockets to ensure the same condition of inputs for every test.

Let’s add the following code to the server.spec.js file:

/* test/server.spec.js *///...var num_tests = 1;describe('----Testing server in the chat', function() {
var socket_1, socket_2;
var username_1 = "ted";
var username_2 = "roni";
var options = { "force new connection": true };
beforeEach(function (done) {
// This would set a timeout of 3000ms only for this hook
this.timeout(3000);
console.log(">> Test #" + (num_tests++)); socket_1 = io("http://localhost:3000", options);
socket_2 = io("http://localhost:3000", options);
socket_1.on("connect", function() {
console.log("socket_1 connected");
socket_1.emit("join", {"sender": username_1, "action": "join"});
});
socket_2.on("connect", function() {
console.log("socket_2 connected");
socket_2.emit("join", {"sender": username_2, "action": "join"});
});
setTimeout(done, 500); // Call done() function after 500ms }); // End beforeEach() afterEach(function (done) {
// This would set a timeout of 2000ms only for this hook
this.timeout(2000);
socket_1.on("disconnect", function() {
console.log("socket_1 disconnected");
});
socket_2.on("disconnect", function() {
console.log("socket_2 disconnected\n");
});
socket_1.disconnect();
socket_2.disconnect();
setTimeout(done, 500); // Call done() function after 500ms }); // End afterEach()}); // End describe()

‘Notify that a user joined the chat’ test

Now, it’s time to add the first test:

/* test/server.spec.js *///...describe('----Testing server in the chat', function() {  //...  it('Notify that a user joined the chat', function(done) {
socket_1.emit("join", {"sender": username_1, "action": "join"});
socket_2.on("join", function(data) {
expect(data.sender).to.equal(username_1);
done();
});
});
});

Mocha inspects the function we pass to hooks or tests. If that function takes an argument, such as done, Mocha assumes that argument is a callback function that we’ll call to indicate our hook/test is finished.

There are a few things to note about the done() callback:

  • It must be called for Mocha to terminate a hook/test and move on to the next one; otherwise, the hook/test keeps running until the timeout reaches.
  • It should not be called twice within a hook/test block. Calling it multiple times will throw an error.
  • If we call done() without argument, we’re telling Mocha that our hook/test succeed. If we pass an argument to it, Mocha assumes that argument is an error.

‘Broadcast a message to others in the chat’ test

Here’s what it should look like:

/* test/server.spec.js *///...describe('----Testing server in the chat', function() {  //...  it('Broadcast a message to others in the chat', function(done) {
var msg_hello = "hello socket_2";
socket_1.emit("broadcast", {"sender": username_1, "action": "broadcast", "msg": msg_hello});
socket_2.on("broadcast", function(data) {
expect(data.msg).to.equal(msg_hello);
done();
});
});
});

‘List all users in the chat’ test

/* test/server.spec.js *///...describe('----Testing server in the chat', function() {  //...  it('List all users in the chat', function(done) {
socket_1.emit("list", {"sender": username_1, "action": "list"});
socket_1.on("list", function(data) {
expect(data.users).to.be.an('array').that.includes(username_1);
expect(data.users).to.be.an('array').that.includes(username_2);
done();
});
});
});

‘A user quit the chat’ test

/* test/server.spec.js *///...describe('----Testing server in the chat', function() {  //...  it('A user quit the chat', function(done) {
socket_1.emit("quit", {"sender": username_1, "action": "quit"});
socket_2.emit("list", {"sender": username_2, "action": "list"});
socket_2.on("list", function(data) {
expect(data.users).to.be.an('array').that.not.includes(username_1);
expect(data.users).to.be.an('array').that.includes(username_2);
done();
});
});
});

‘Notify that a user quit the chat’ test

/* test/server.spec.js *///...describe('----Testing server in the chat', function() {//...it('Notify that a user quit the chat', function(done) {
socket_1.emit("quit", {"sender": username_1, "action": "quit"});
socket_2.on("quit", function(data) {
expect(data.sender).to.equal(username_1);
done();
});
});
});

‘Send a private message to a user’ test

/* test/server.spec.js *///...describe('----Testing server in the chat', function() {  //...  it('Send a private message to a user', function(done) {
var msg_hello = "hello socket_2";
socket_1.emit("send", {"sender": username_1, "action": "send", "receiver": username_2, "msg": msg_hello});
socket_2.on("send", function(data) {
expect(data.receiver).to.equal(username_2);
expect(data.msg).to.equal(msg_hello);
done();
});
});
});

Testing server in a group

beforeEach() and afterEach() test hook

/* test/server.spec.js *///...describe('----Testing server in a group', function() {
var socket_1, socket_2;
var username_1 = "ted";
var username_2 = "roni";
var group_name = "doctors";
var options = { "force new connection": true };
beforeEach(function (done) {
// This would set a timeout of 3000ms only for this hook
this.timeout(3000);
console.log(">> Test #" + (num_tests++)); socket_1 = io("http://localhost:3000", options);
socket_2 = io("http://localhost:3000", options);
socket_1.on("connect", function() {
console.log("socket_1 connected");
socket_1.emit("join", {"sender": username_1, "action": "join"});
socket_1.emit("join_group", {"sender": username_1, "action": "join_group", "group": group_name});
}); socket_2.on("connect", function() {
console.log("socket_2 connected");
socket_2.emit("join", {"sender": username_2, "action": "join"});
socket_2.emit("join_group", {"sender": username_2, "action": "join_group", "group": group_name});
});
setTimeout(done, 500); // Call done() function after 500ms }); // End beforeEach() afterEach(function (done) {
// This would set a timeout of 2000ms only for this hook
this.timeout(2000);
socket_1.on("disconnect", function() {
console.log("socket_1 disconnected");
});
socket_2.on("disconnect", function() {
console.log("socket_2 disconnected\n");
});
socket_1.disconnect();
socket_2.disconnect();
setTimeout(done, 500); // Call done() function after 500ms }); // End afterEach()}); // End describe()

‘Notify that a user joined a group’ test

/* test/server.spec.js *///...describe('----Testing server in a group', function() {  //...  it('Notify that a user joined a group', function(done) {
socket_1.emit("join_group", {"sender": username_1, "action": "join_group", "group": group_name});
socket_1.on("join_group", function(data) {
expect(data.sender).to.equal(username_1);
expect(data.group).to.equal(group_name);
done();
});
});
});

‘Broadcast a message to a group’ test

/* test/server.spec.js *///...describe('----Testing server in a group', function() {  //...  it('Broadcast a message to a group', function(done) {
var msg_hello = "hello socket_2";
socket_1.emit("broadcast_group", {"sender": username_1, "action": "broadcast_group", "group": group_name, "msg": msg_hello});
socket_2.on("broadcast_group", function(data) {
expect(data.sender).to.equal(username_1);
expect(data.group).to.equal(group_name);
expect(data.msg).to.equal(msg_hello);
done();
});
});
});

‘List all users that are inside a group’ test

/* test/server.spec.js *///...describe('----Testing server in a group', function() {  //...  it('List all clients that are inside a group', function(done) {
socket_1.emit("list_members_group", {"sender": username_1, "action": "list_members_group", "group": group_name});
socket_1.on("list_members_group", function(data) {
expect(data.sender).to.equal(username_1);
expect(data.group).to.equal(group_name);
expect(data.members).to.be.an('array').that.includes(username_1);
expect(data.members).to.be.an('array').that.includes(username_2);
done();
});
});
});

‘List the existing groups’ test

/* test/server.spec.js *///...describe('----Testing server in a group', function() {  //...  it('List the existing groups', function(done) {
socket_1.emit("list_groups", {"sender": username_1, "action": "list_groups"});
socket_1.on("list_groups", function(data) {
expect(data.sender).to.equal(username_1);
expect(data.groups).to.be.an('array').that.includes(group_name);
done();
});
});
});

Conclusion

--

--

No responses yet