Confirming the outcome of an unawaited asynchronous function in JavaScript

Consider the code snippet below for creating fake accounts:

export const accounts = [];
export const emails = [];

export async function createAccount(name, email) {
    accounts.push({name, email});
    void sendEmail(email);
}

async function sendEmail(email) {
    setTimeout(0, () => emails.push(`Account created for ${email}`));
}

The createAccount() function saves an account in the array and also sends an email reporting the account creation. It is important to note that the createAccount() function does not wait for the email to be sent as it may take some time to complete.

However, when we attempt to test the email sending functionality:

import {assert} from 'chai';
import {createAccount, emails} from './index.js';

describe('test', async () => {
  it('test', async() => {
    await createAccount('John Doe', 'johnDoe@example.com');
    assert(emails[0], 'Account created for johnDoe@example.com');
  });
});

Unfortunately, the test does not pass as expected...

$ npm run test

> test
> mocha test.js



  test
    1) test


  0 passing (5ms)
  1 failing

  1) test
       test:
     AssertionError: Account created for johnDoe@example.com
      at Context.<anonymous> (file:///home/me/sandbox/foo/test.js:7:5)

The reason the test fails is because the sendEmail() function is called asynchronously and may not have sent the email yet.

How can we effectively test if the email was eventually sent without:

  • revealing the return value of sendEmail();
  • relying on delays or timeouts.

Any suggestions on how to achieve this?

Appendix

To simplify testing, you can use the following package.json:

{
  "name": "foo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "mocha test.js"
  },
  "type": "module",
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "chai": "^5.1.1",
    "mocha": "^10.6.0"
  }
}

Answer №1

Modify createAccount to return a promise for the account creation result. This result includes another promise for the email sending result. Similar to the fetch pattern where the response object only contains headers and the body content needs to be awaited separately.

export async function createAccount(name, email) {
  accounts.push({name, email});
  const emailSent = sendEmail(email); // .then(x => void x) if you don't want to leak further details
  emailSent.catch(e => { /* ignore */ });
  return { emailSent };
}

async function sendEmail(email) {
  await new Promise(resolve => { setTimeout(0, resolve); });
  emails.push(`Account created to ${email}`));
}
describe('test', async () => {
  it('test', async() => {
    const { emailSent } = await createAccount('John Doe', '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="ccb8a9bfb88ca9b4ada1bca0a9e2afa3a1">[email protected]</a>');
    assert(accounts[0], '…');
    await emailSent;
    assert(emails[0], 'Account created to <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="285c4d5b5c684d50494558444d064b4745">[email protected]</a>');
  });
})

Alternatively, instead of having a promise on the result which someone "should" await, you can have a notify method that the caller can (but doesn't need to) call and can await or forget.

export async function createAccount(name, email) {
  accounts.push({name, email});
  return {
    notify() {
      await sendEmail(email);
    },
  };
}

<!-->
describe('test', async () => {
  it('test', async() => {
    const account = await createAccount('John Doe', '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="9beffee8efdbfee3faf6ebf7feb5f8f4f6">[email protected]</a>');
    assert(accounts[0], '…');
    await account.notify();
    assert(emails[0], 'Account created to <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="b2c6d7c1c6f2d7cad3dfc2ded79cd1dddf">[email protected]</a>');
  });
});

This puts more responsibility on the caller to not forget .notify() of course. If you need, provide another helper method wrapping this behaviour.


If you don't want to expose those internals at all, you can of course still test these internals separately. Export saveAccount and sendEmail from the implementation module, test them separately. Then for testing the public createAccount, just mock them and check they are being called. This way, you can even test that createAccount does in fact not wait for sendEmail.

Answer №2

  1. One option is to implement a callback that is triggered once the email has been successfully sent.
  2. An alternative approach is to verify the side effects, such as intercepting the call to the email API.
  3. Another strategy is to have your createAccount function return a promise that only resolves after the email has been sent. You can also include a separate property like emailSentPromise in the returned object.
  4. Consider using mocking or dependency injection to allow the caller to substitute their own implementation of sendEmail instead of using the default one.

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

Vue should only activate the element that has been clicked on

I'm currently working on my first Vue project and encountering an issue with triggering a child component within a table cell. Whenever I double click a cell, the `updated` event is being triggered in all child components associated with the table cel ...

React-Native 0.1.17 Navigator Bar: Enhancing User Navigation Experience

An issue arose after upgrading my react-native 0.1.15 app to version 0.1.17 - I'm now encountering an 'Unable to download JS bundle error'. Upon investigation, I found the error in my code: var SportsSocial = React.createClass({ component ...

What is the most efficient way to retrieve a document from pouchdb that includes the revision attribute?

I am currently developing an application using the electron framework and integrating pouchdb. As certain values in my database are dynamic and constantly changing, I am looking for a way to track these changes without having to create new documents for ea ...

Passing conditional empty variables through `res.render`

I am currently facing a challenge with passing multiple variables into a res.render. One of the variables will have an object to send through, while the other may not have anything to pass. As a result, I am encountering an undefined error. Below is the ...

What is the best way to populate all data in select2 (4.0) upon page load?

I'm currently utilizing the select2 plugin (v.4.0) and have a specific goal in mind: $("#search-input-chains").select2({ placeholder: "Unit", theme: "bootstrap4", ...

Using various conditions and operators to display or conceal HTML elements in React applications, particularly in NextJS

I am seeking ways to implement conditional logic in my React/Next.js web app to display different HTML elements. While I have managed to make it work with individual variable conditions, I am encountering difficulties when trying to show the same HTML if m ...

Executing JavaScript code within a class object using ASP-VB

I'm looking to create a function that will show a Javascript-based notification. I already have the code for the notification, but I'm trying to encapsulate it in a class library as a function. However, I am unsure about what to return from the f ...

Forwarding users to a new destination through a script embedded within a frame

Currently, I am facing an issue where a page (lobby_box.php) is being loaded every 4 seconds on my main page (index.php) using JavaScript. The problem arises when the code within (lobby_box.php) that is meant to redirect the client from index.php to anothe ...

Setting up the current user's location when loading a map with Angular-google-maps

I am currently utilizing the library in conjunction with the IONIC framework. To manually set the center of the map, I have implemented the following code snippet: .controller('mainCtrl', function($scope) { $scope.map = { cen ...

Bootstrap: Retrieve an image from a modal

I am working on a modal that contains a variety of selectable images. When an image is clicked, I want to change the text of the button in the modal to display the name of the selected image. Additionally, I would like to grab the selected image and displa ...

Setting up the Firebase emulator: should you use getFirestore() or getFirestore(firebaseApp)?

After delving into the process of connecting your app to the Firebase emulators like Firestore emulator, I came across the primary documentation which outlined the steps for Web version 9: import { getFirestore, connectFirestoreEmulator } from "fireba ...

Comparing the functions of useMemo and the combination of useEffect with useState

Is there a benefit in utilizing the useMemo hook instead of using a combination of useEffect and useState for a complex function call? Here are two custom hooks that seem to function similarly, with the only difference being that useMemo initially returns ...

The declaration of 'exports' is not recognized within the ES module scope

I started a new nest js project using the command below. nest new project-name Then, I tried to import the following module from nuxt3: import { ViteBuildContext, ViteOptions, bundle } from '@nuxt/vite-builder-edge'; However, I encountered th ...

"Viewed By" aspect of Facebook communities

I'm working on implementing a feature that shows when a post has been seen, similar to Facebook groups, using JS and PHP. I've been able to track the number of times a post has been seen through scrolling actions, but now I want to determine if u ...

Typescript: Subscribed information mysteriously disappeared

[ Voting to avoid putting everything inside ngOnit because I need to reuse the API response and model array in multiple functions. Need a way to reuse without cluttering up ngOnInit. I could simply call subscribe repeatedly in each function to solve the p ...

Serve up a 400 error response via the express server when hitting a

I need help serving a 400 error for specific files within the /assets directory that contain .map in their names. For example: /assets/foo-huh4hv45gvfcdfg.map.js. Here's the code I tried, but it didn't work as expected: app.get('/assets&bs ...

Engage with React JS arrays of objects

I have a specific object structure that looks like the following: [ { "periodname": "Test", "periodtime": "" }, { "periodname": "", "periodtime&quo ...

Vuex - modify store state without changing the original object

Currently, I am encountering an issue where my attempt to duplicate a store object for local modification is causing the original store object to also be changed. The specific method I am using is ...mapState["storeObject"] To illustrate, here is a breakd ...

Only the initial TinyMCE editor that was added dynamically is visible, the rest are not showing

I am currently developing a javascript application that will showcase a real-time discussion thread. The app will regularly communicate with the server to fetch new data and display it on the page. To enable users to post comments and replies, we have deci ...

A straightforward Node.js function utilizing the `this` keyword

When running the code below in a Chrome window function test(){ console.log("function is " + this.test); } test(); The function test is added to the window object and displays as function is function test(){ console.log("function is " + this.tes ...