DEV Community

Julien
Julien

Posted on

Demystifying NestJS WebSocket Gateways: A Step-by-Step Guide to Effective Testing

Introduction

In the ever-evolving world of real-time applications, WebSocket communication has emerged as a powerful tool to establish a seamless and efficient connection between clients and servers. NestJS, a popular TypeScript framework, has incorporated WebSocket Gateways to simplify real-time communication within its ecosystem. While implementing WebSocket functionality using NestJS is undoubtedly exciting, ensuring that it works reliably and efficiently is equally crucial. That's where testing plays a pivotal role.

Welcome to this comprehensive guide on creating tests for a NestJS WebSocket Gateway. In this article, we will dive into the fundamentals of WebSocket Gateways and demonstrate how to craft robust and effective tests to validate their behavior.

Whether you're a seasoned NestJS developer looking to expand your knowledge or a newcomer keen on exploring WebSocket Gateways, this article will provide you with valuable insights and practical examples to start mastering the art of testing real-time functionality in your NestJS applications.

Part 1: Setting Up Your NestJS WebSocket Gateway: A Step-by-Step Guide

In this first part of our guide, we will walk you through the process of setting up a NestJS application with WebSocket capabilities and generating a chat gateway. By the end of this section, you'll have a solid foundation to start building your WebSocket-based applications and writing tests for them.

Step 1: Installing NestJS CLI

To begin, ensure you have Node.js installed on your machine.

If you haven't already installed the NestJS Command Line Interface (CLI), you can do so globally by running the following command:

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Step 2: Creating a New Nest Application

Once the NestJS CLI is installed, create a new Nest application by executing the following command:

nest new nest-app
Enter fullscreen mode Exit fullscreen mode

During the setup process, you'll be prompted to choose a package manager. For this tutorial, we'll be using npm, so go ahead and select it when prompted.

Image description

Image description

After the application is generated, navigate into the project directory using the following command:

cd nest-app
Enter fullscreen mode Exit fullscreen mode

Step 3: Installing Required Packages

In order to add WebSocket functionality to our application, we need to install the necessary packages. Run the following command to install @nestjs/websockets and @nestjs/platform-socket.io:

npm i --save @nestjs/websockets @nestjs/platform-socket.io
Enter fullscreen mode Exit fullscreen mode

Step 4: Generating the Chat Gateway

With the required packages installed, let's create the chat gateway. The gateway will handle WebSocket connections and facilitate real-time communication. Use the Nest CLI to generate the chat gateway boilerplate code:

nest generate gateway chat
Enter fullscreen mode Exit fullscreen mode

Image description

This command will create new files named chat.gateway.ts and chat.gateway.spec.ts under the src/chat/ directory, which you can open with a text editor of your choice:

vim src/chat/chat.gateway.ts
Enter fullscreen mode Exit fullscreen mode

With the chat gateway file open, you'll find a basic structure for the gateway, including a class definition with decorators for WebSocketGateway. We will build upon this structure and add more functionality as we progress through the article.

Congratulations! At this point, you have successfully set up your NestJS application with WebSocket capabilities and generated a chat gateway. In the next section, we will dive deeper into the WebSocket Gateway and begin writing tests to ensure its proper functionality.

Part 2: Implementing the Chat Gateway and Understanding the Code

In this section, we'll replace the boilerplate code with a more feature-rich implementation of the ChatGateway. We'll explore each part of the code, understand its purpose, and discuss the changes we've made.

Before we dive into the code, let's briefly go over the purpose of each interface and decorator used in the gateway:

  • @WebSocketGateway(): This decorator marks the class as a WebSocket gateway, allowing it to handle WebSocket connections and events.

  • @WebSocketServer(): This decorator injects the WebSocket server instance (Socket.io instance) into the gateway, enabling direct communication with connected clients.

  • OnGatewayInit: This interface provides a method afterInit() that gets executed when the WebSocket gateway is initialized. It's a great place to perform any setup or logging related to gateway initialization.

  • OnGatewayConnection: This interface provides a method handleConnection(client: any, ...args: any[]) that gets called when a new WebSocket connection is established. Here, you can handle tasks related to the client's connection, like logging or broadcasting a welcome message.

  • OnGatewayDisconnect: This interface provides a method handleDisconnect(client: any) that gets triggered when a WebSocket client disconnects. You can use this method to perform any cleanup tasks or log the disconnection event.

  • @SubscribeMessage('ping'): This decorator indicates that the method handleMessage(client: any, data: any) will handle messages with the event name 'ping' sent from the client. In our case, it simply logs the received message and returns a response event named 'pong' with a fixed response data.

Now, let's take a closer look at the code changes we've made to the ChatGateway:

import { Logger } from "@nestjs/common";
import {
  OnGatewayConnection,
  OnGatewayDisconnect,
  OnGatewayInit,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from "@nestjs/websockets";

import { Server } from "socket.io";

@WebSocketGateway()
export class ChatGateway
  implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
  private readonly logger = new Logger(ChatGateway.name);

  @WebSocketServer() io: Server;

  afterInit() {
    this.logger.log("Initialized");
  }

  handleConnection(client: any, ...args: any[]) {
    const { sockets } = this.io.sockets;

    this.logger.log(`Client id: ${client.id} connected`);
    this.logger.debug(`Number of connected clients: ${sockets.size}`);
  }

  handleDisconnect(client: any) {
    this.logger.log(`Cliend id:${client.id} disconnected`);
  }

  @SubscribeMessage("ping")
  handleMessage(client: any, data: any) {
    this.logger.log(`Message received from client id: ${client.id}`);
    this.logger.debug(`Payload: ${data}`);
    return {
      event: "pong",
      data: "Wrong data that will make the test fail",
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

In the code above, we've replaced the previous boilerplate gateway with a more functional implementation:

  1. We imported the required modules and classes from @nestjs/common and @nestjs/websockets, as well as the Server class from 'socket.io' for WebSocket communication.

  2. We defined a logger using @nestjs/common to facilitate logging within the gateway. This logger will be helpful during testing to monitor the events.

  3. Inside the afterInit() method, we log a simple message indicating that the WebSocket gateway has been initialized. This can be useful for tracking the application's lifecycle.

  4. In the handleConnection(client: any, ...args: any[]) method, we handle a new WebSocket connection. We log the client's ID and the total number of connected clients to keep track of active connections.

  5. The handleDisconnect(client: any) method is responsible for logging disconnection events. When a WebSocket client disconnects, this method will be called to perform any necessary cleanup or logging.

  6. We implemented a @SubscribeMessage('ping') method named handleMessage(client: any, data: any). This method handles incoming messages with the event name 'ping' from connected clients. It logs the received message along with its payload and returns a fixed response event named 'pong' with a data field that contains an intentionally wrong message. We've added this intentional mistake to demonstrate how we can test for erroneous behavior in the upcoming section.

With the new ChatGateway in place, our WebSocket application is now equipped to handle connections, disconnections, and incoming messages from clients. In the next part of the article, we'll focus on writing tests for this gateway to ensure its proper functionality and robustness.

Part 3: Writing Tests for the NestJS WebSocket Gateway

In this final part of our guide, we'll explore how to write tests for the ChatGateway we created earlier. By the end of this section, you'll have a suite of tests to ensure your WebSocket gateway functions reliably and as expected.

Let's open the test file:

vim src/chat/chat.gateway.spec.ts
Enter fullscreen mode Exit fullscreen mode

And replace boilerplate code with the following tests:

import { Test } from "@nestjs/testing";
import { ChatGateway } from "./chat.gateway";
import { INestApplication } from "@nestjs/common";
import { Socket, io } from "socket.io-client";

async function createNestApp(...gateways: any): Promise<INestApplication> {
  const testingModule = await Test.createTestingModule({
    providers: gateways,
  }).compile();
  return testingModule.createNestApplication();
}

describe("ChatGateway", () => {
  let gateway: ChatGateway;
  let app: INestApplication;
  let ioClient: Socket;

  beforeAll(async () => {
    // Instantiate the app
    app = await createNestApp(ChatGateway);
    // Get the gateway instance from the app instance
    gateway = app.get<ChatGateway>(ChatGateway);
    // Create a new client that will interact with the gateway
    ioClient = io("http://localhost:3000", {
      autoConnect: false,
      transports: ["websocket", "polling"],
    });

    app.listen(3000);
  });

  afterAll(async () => {
    await app.close();
  });

  it("should be defined", () => {
    expect(gateway).toBeDefined();
  });

  it('should emit "pong" on "ping"', async () => {
    ioClient.connect();
    ioClient.emit("ping", "Hello world!");
    await new Promise<void>((resolve) => {
      ioClient.on("connect", () => {
        console.log("connected");
      });
      ioClient.on("pong", (data) => {
        expect(data).toBe("Hello world!");
        resolve();
      });
    });
    ioClient.disconnect();
  });
});
Enter fullscreen mode Exit fullscreen mode

In this test file, we use the @nestjs/testing package to set up a testing module and compile it to create a NestJS application instance. The createNestApp() function takes the ChatGateway as an argument and returns the initialized application.

We then create a Socket.io client (ioClient) that connects to the WebSocket server at http://localhost:3000. The autoConnect: false option ensures the client doesn't automatically connect, and we specify the transports as "websocket" and "polling" so that websocket connection is the default try to connect option.

Now, let's walk through the two tests we have written:

  1. The first test simply checks if the ChatGateway is defined. We expect the gateway to be defined after it's instantiated. This ensures that the gateway is set up properly and ready to handle WebSocket connections.

  2. The second test simulates a "ping" event from the client to the server. We expect the server to respond with a "pong" event containing the same data that was sent in the "ping" event. However, the current implementation of handleMessage() returns a wrong data in the response, which causes the test to fail.

To run the tests, first install socket.io-client package:

npm install -D socket.io-client
Enter fullscreen mode Exit fullscreen mode

Note: You do not need to install @types/socket.io-client types since it's already in the main package.

And use the following command in another shell window:

npm run test:watch
Enter fullscreen mode Exit fullscreen mode

With the test suite running in watch mode, any changes made to the ChatGateway or the test file will trigger the tests to rerun automatically, ensuring that your WebSocket gateway stays robust and properly tested throughout development.

One test fails as expected.

To fix the bug in the gateway's handleMessage() method, update it as follows:

@SubscribeMessage("ping")
handleMessage(client: any, data: any) {
  this.logger.log(`Message received from client id: ${client.id}`);
  this.logger.debug(`Payload: ${data}`);
  return {
    event: "pong",
    data,
  };
}
Enter fullscreen mode Exit fullscreen mode

Now, with the correct implementation, the test should pass successfully.

Congratulations! You've successfully set up and tested a NestJS WebSocket Gateway. Armed with the knowledge of how to test WebSocket functionality, you can confidently develop real-time applications with NestJS, knowing that your WebSocket communication is reliable and efficient.

Thank you for joining me on this journey to explore WebSocket Gateways with NestJS! I hope you found this guide informative and valuable for your real-time application development endeavors.

If you enjoyed reading this article and found it helpful, please consider giving it a thumbs-up or leaving a comment below. Your feedback and engagement encourage me to create more content like this, helping the community learn and grow together.

Happy coding, and may your WebSocket-powered applications thrive with NestJS! 🚀🌟

Top comments (3)

Collapse
 
clivassy profile image
Julia Batoro

Thank's a lot for this enlightening article! Can't wait to read the next ones 🌟

Collapse
 
coviccinelle profile image
thi-phng

Super useful! Thanks a bunch 🌻

Collapse
 
dgaloiu profile image
dgaloiu

Wow, this was very helpfull ! Keep going :)