mediumEngineering Practices#72

Testing async code

Prompt

Write a Jest test for an async function fetchUserName. Test that it resolves to the correct name and that it rejects on error.

Solution

it('returns Alice for id 1', async () => {
  const name = await fetchUserName(1)
  expect(name).toBe('Alice')
})

it('throws for negative id', async () => {
  await expect(fetchUserName(-1)).rejects.toThrow('Invalid id')
})
Mentor's take

Async tests are where suites silently rot, because the failure mode is a test that passes vacuously. If you forget to await — or forget to return the promise — the test function exits before the assertion runs, Jest marks it green, and you now have a test that would stay green if the implementation were deleted. That's worse than no test: it's false confidence with a maintenance cost.

The two idioms to have cold: await the happy path and assert on the resolved value; use await expect(promise).rejects.toThrow(...) for the failure path. The rejects matcher exists because the naive alternative — try { await fn() } catch (e) { expect(e.message)... } — has a hole: if the function doesn't throw, no assertion runs and the test passes. Jest's answer for the try/catch style is expect.assertions(1), but .rejects makes the intent declarative and closes the hole in one line.

Red flag: testing a rejection with a bare try/catch and no guard against the no-throw path. The interviewer is specifically listening for whether you know that test can pass when the code is broken.

Say it: "I assert rejections with await expect(...).rejects.toThrow because a try/catch without expect.assertions passes silently when the function stops throwing."