What is the best way to ensure that Jest waits for an event to occur before performing an assertion?

I've developed a facade for the nats streaming lib in the following way:

import nats, { Message, Stan, Subscription, SubscriptionOptions } from 'node-nats-streaming'

class NatsHelper {
  private client: Stan | null = null

  public connect(url: string, clusterID: string, clientID: string, listener: (...args: any[]) => void, verboseConnection: boolean = true): void {
    const clientIDString = `${clientID}-${randomBytes(4).toString('hex')}`
    if (verboseConnection) {
      console.log(`Connecting to NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}'`)
    }
    const connectionAttempt = nats.connect(
      clusterID,
      clientIDString,
      {
        url
      }
    )

    const setupConnection = (...args: any[]): void => {
      this.client = connectionAttempt

      this.client.on('close', (): void => {
        if (verboseConnection) {
          console.log(`Connection with NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}' was closed`)
        }
        this.client = null
        process.exit()
      })

      process.on('SIGINT', () => this.client?.close())
      process.on('SIGTERM', () => this.client?.close())

      if (verboseConnection) {
        console.log(`Connected to NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}' successfuly`)
      }
      listener(...args)
    }

    connectionAttempt.on('connect', setupConnection)
  }
}

However, I am encountering an issue where I cannot verify if the provided listener function is being called because it depends on the Stan 'connect' event, which Jest completes the test before it occurs.

Is there a way for Jest to wait for this event to occur and then execute the expect function?

Answer №1

It seems like you may have made this more complex than necessary. You can actually write a test for the original code without making any changes by using mocking in Jest to mock out the library and provide mock implementations for your on method. Check out the example below:

import nats from "node-nats-streaming";
import { mock } from "jest-mock-extended";
import { NatsHelper } from "./nats";

jest.mock("node-nats-streaming");

describe("NatsHelper", () => {
  it("calls listener on connectEvent", () => {
    const client = mock<nats.Stan>();

    client.on.mockImplementation((name, callback) => {
      if (name !== "close") {
        callback();
      }
      return client;
    });

    jest.mocked(nats).connect.mockReturnValue(client);

    const connector = new NatsHelper();

    const listener = jest.fn();

    connector.connect("foo", "foo", "foo", listener);

    expect(listener).toHaveBeenCalled();
  });
});

Answer №2

[UPDATE] Discovered a solution that addresses my query. It turns out we have the ability to "transform" an event into a Promise, like so:

import { randomBytes } from 'crypto'
import nats from 'node-nats-streaming'

export class NullClientError extends Error {
  constructor() {
    super('Nats client is not connected')
    this.name = 'NullClientError'
  }
}

export class NatsHelper {
  private verboseConnectionString: string
  private client: nats.Stan
  private connector: nats.Stan

  constructor(
    private readonly verboseConnection: boolean = true
  ) { }

  public async connect(url: string, clusterID: string, clientID: string, callback: (...args: any[]) => void): Promise<void> {
    const clientIDString = `${clientID}-${randomBytes(4).toString('hex')}`
    this.verboseConnectionString = `NATS cluster '${clusterID}' with clientID '${clientIDString}' on url '${url}'`
    if (this.verboseConnection) {
      console.log(`Establishing connection to ${this.verboseConnectionString}`)
    }

    this.connector = nats.connect(
      clusterID,
      clientIDString,
      {
        url
      }
    )

    this.connector.on('connect', (...args: any[]) => {
      const realCallback = this.setupListener(callback)
      realCallback(...args)
    })

    return await new Promise(
      resolve => {
        if (this.connector) {
          this.connector.on('connect', () => {
            resolve()
          })
        }
      }
    )
  }

  private setupListener(listener: (...args: any[]) => void): (...args: any[]) => void {
    const setupConnection = (...args: any[]): void => {
      if (this.connector === undefined) {
        throw new NullClientError()
      }

      this.client = this.connector

      if (this.client === undefined) {
        throw new NullClientError()
      }

      this.client.on('close', (): void => {
        if (this.verboseConnection) {
          console.log(`The connection with ${this.verboseConnectionString} has been terminated`)
        }
        process.exit()
      })

      process.on('SIGINT', () => this.client?.close())
      process.on('SIGTERM', () => this.client?.close())

      if (this.verboseConnection) {
        console.log(`Successfully connected to ${this.verboseConnectionString}`)
      }
      listener(...args)
    }
    return setupConnection
  }
}

To validate it, run asynchronous tests as follows:

describe('NatsHelper', () => {
  test('verify NatsHelper invokes connect with accurate parameters', async () => {
    const connectSpy = jest.spyOn(nats, 'connect')
    const sut = new NatsHelper(false)
    const { url, clusterID, clientID, listener } = makeConnectionParams()
    await sut.connect(url, clusterID, clientID, listener)
    const clientIDString = connectSpy.mock.calls[0][1]
    expect(clientIDString).toContain(clientID)
    expect(connectSpy).toHaveBeenCalledWith(clusterID, clientIDString, { url })
  })

  test('ensure NatsHelper transfers the callback upon successful connection', async () => {
    const connectionParms = makeConnectionParams()
    const { url, clusterID, clientID } = connectionParms
    const listenerSpy = jest.spyOn(connectionParms, 'listener')
    const sut = new NatsHelper(false)
    await sut.connect(url, clusterID, clientID, connectionParms.listener)
    expect(listenerSpy).toHaveBeenCalledTimes(1)
  })
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Automatically updating the user interface with fresh data using Jade, Express.js, and NodeJS

The JSON Object (stories) appears as follows: [ { title: "Journey of Mastering Python", id: "1", description: "Challenging, Rewarding, Fulfilling" }] The PUG template for displaying the stories is structured ...

When a hyperlink is clicked, an image appears in the div

Can someone help me with a javascript method that can display an image in a div based on which href link is clicked? I've started writing some code, but I think it needs further development. <script type=" "> function changeImg(){ var ima ...

Getting the correct JSON field value from a JavaScript Promise object in the right way

Trying to fetch data in JSON format from a URL, I encountered an issue where the value of responseData.title differed between two calls made within the second .then() function below: var getTitleForId = async function(id) { if (!id) return fal ...

Initiate timer when mouse leaves; pause timer upon mouse hovering using JavaScript

I am currently working on creating a volume bar for a video. The idea is that when you click the volume button, a div will appear allowing you to control the volume level. As soon as you hover out of the div, a timer will start counting down from 7 and the ...

Is the jQuery AJAX call using POST method being retrieved as $_GET?

Below is a snippet of code that I've successfully implemented: <script type="text/javascript"> $(document).ready(function() { // Initializing the table $('#table_1').tableDnD({ onDrop: function(table, row) { $.tab ...

Asking for access to a Node server with React authentication

Let me outline the current scenario: I am working with a database that contains registered usernames and passwords. My task at hand is to create a login form where users can input their username and password to log in. These credentials are already stored ...

Is it possible to change a CSS variable using VueJS?

With CSS Modules, we are able to utilize variables in both our CSS and JS code. // Using it in template ... ...<p :class="{ [$style.red]: isRed }">Am I red?</p> // Using it in JS ... ... <script> export default { created ( ...

Combining a random selection with a timer in JavaScript for a dynamic user experience

Currently, I am developing a Twitter bot using JavaScript, Node.js, and the Twit package. The goal is for the bot to tweet every 60 seconds with a randomly selected sentence from an array. When testing the timer and random selection function individually, ...

Utilizing browser back functionality to dismiss a layer: A step-by-step guide

When I click a button, a full page layer opens up for filtering. While the layer can be closed using a button, some users prefer to use the browser's navigation functionality causing it to load the last page instead of the one that opened the filter. ...

Facing a challenge with handling HTTP data in a TypeScript-based Angular web application

I am currently working on developing a web application using Angular and the SpringMVC Framework. One of the tasks I'm facing is loading a list of users (referred to as "consulenti" in the code). While the backend HTTP request works fine, I encounter ...

Multiple layers of SVG text with consistent widths and dynamic heights

I have a challenge in mind where I want to implement a text effect that automatically adjusts its width while maintaining proportional heights. My goal is to stack multiple words on top of each other to create a visual style similar to the example image b ...

In Java, use an HttpRequest to retrieve the webpage content and ensure that the process waits until the JavaScript has finished

Is there a way in Java to wait for partial content of a webpage to be loaded by JavaScript before initiating the rest of the process? The webpage contains script that loads additional content once the page is fully loaded, and I need to ensure that the p ...

Guide to using prop types with a Typescript function and integrating them with a Material-UI button

Apologies in advance if I'm not articulating myself clearly; I'm still trying to wrap my head around Typescript. I have created a styled button using Material-UI, but I am unsure of how to make it reusable across the entire application. My goal ...

Creating a flexible route path with additional query parameters

I am facing a challenge in creating a unique URL, similar to this format: http://localhost:3000/boarding-school/delhi-ncr However, when using router.push(), the dynamic URL is being duplicated like so: http://localhost:3000/boarding-school/boarding-school ...

Why am I unable to use a string as the src in next/image component?

After importing the Image module with the code import Image from "next/image";, I encountered an error that states: The type '{ src: string; }' cannot be assigned to type 'IntrinsicAttributes & ImageProps'. The type &apo ...

The placeholder text is not displaying with bullets in Internet Explorer. However, it is functioning correctly in Chrome

I need assistance displaying the placeholder text in IE8 as shown below: "Any relevant reference numbers, such as Direct Debits: - Name of the Branch (if applicable) - What was the original problem - Date the problem occurred " While it appears correct ...

Establish the focus when the TinyMCE editor loads

After clicking the edit button, the tinymce text editor is displayed but there's an issue where the focus is not set on the first load of the editor. However, it works fine from the second load onwards. Here's what I have attempted: HTML <h ...

Next.js 11 Script integration for TruConversion Heatmap Tracking

I'm in the process of setting up TruConversion's heatmap script on my Next.js 11 website. According to TruConversion's website, the script should be added before the closing </head> tag. However, Next.js documentation advises against ...

Issues with rendering Google Maps on google-maps-react persists, stuck endlessly in loading phase

After following the tutorial for google-maps-react, I attempted to display a Google Map in my app using the same structure as the example. However, the map is not rendering. Link to Tutorial There are no errors showing up in my console. Here is the dire ...

Is there a better way to handle passing named arguments to a JavaScript Function? Any potential challenges that may arise

This question is closely related to, but I believe not a duplicate of, Passing named arguments to a Javascript function [duplicate] and Named parameters in javascript. Various responses and discussions on those posts suggest different approaches to handle ...