SkillByAIOpen interactive version →

Lesson 7 / 25

Testing Promises and async/await

Return or await, and test rejections too.

The runner must wait for the promise

If a test starts async work and finishes without waiting, the runner marks it passed before the assertion runs, or the failure surfaces in a later test. Make the test function async and await the code, or return the promise. For the happy path, await and assert on the value, or use await expect(promise).resolves.toEqual(...). For failures, use await expect(promise).rejects.toThrow('...'); forgetting the await here is a classic bug that makes the test pass no matter what. expect.assertions(n) asserts that exactly n assertions ran, which guards try/catch style tests where the catch might never execute.

Make the clock and the network do what you say

Asynchronous code and time-dependent code become deterministic when the test controls promises, timers, dates and randomness.

Figure 3.1 — Await promises, fake the timers, pin dates and random values.

Resolves, rejects and assertion counting

fetchUser returns a promise.

import { it, expect } from 'vitest';
import { fetchUser, parseAge } from './users';

it('loads a user', async () => {
  const user = await fetchUser('42');
  expect(user.name).toBe('Asha');
});

it('loads a user (resolves form)', async () => {
  await expect(fetchUser('42')).resolves.toMatchObject({ id: '42' });
});

it('rejects an unknown id', async () => {
  await expect(fetchUser('missing')).rejects.toThrow('User not found');
  //  ^ without this await the test can pass even if fetchUser resolves
});

it('reports a parse error', async () => {
  expect.assertions(1);
  try {
    await parseAge('abc');
  } catch (err) {
    expect(err).toBeInstanceOf(RangeError);
  }
});

Turn on the floating-promise lint

The typescript-eslint rule no-floating-promises flags an expect(...).rejects (or any promise) that is not awaited or returned, catching this bug before the test ever runs.

Quick check: What happens if you forget await before expect(p).rejects.toThrow()?

  • The test can finish and pass before the assertion is checked
  • The runner automatically awaits it
  • TypeScript refuses to compile
  • The promise is cancelled
Answer

The test can finish and pass before the assertion is checked — Always await or return async assertions.