Recent chats
ASKGEEZ Q&A GUIDETechnology & devices

How can I test async code that succeeds, rejects, or returns empty data?

Quick answer

Use a controlled test double so the async function receives predictable local data instead of calling a real service. Write separate tests for a normal result, a rejected operation, and a successful operation that returns an empty collection. Decide what each outcome should mean for your function, then assert its returned value or error. A stub can provide controlled data and simulate failures and edge cases; use the setup required by your chosen test framework. [S1]

LET’S WORK THROUGH IT

The question, the details, the next step.

Question

What should I test, and how do I keep the tests independent of a real API?

Geez

Start with three cases for the function’s main dependency: it resolves with ordinary data, rejects with an error, or resolves with an empty result. Use a local stub—a substitute that returns data you control—rather than real credentials or a live service. Stubs can simulate success, failures, and edge cases; mocks can also record calls. [S1]

Treat these as distinct outcomes. Empty data is not automatically a failure: it may be a valid result that your function should pass through or turn into a friendly message. Write down the intended behavior before choosing assertions. The source guidance discusses Node.js’s built-in test runner, Jest, and Vitest, but it does not establish one framework’s exact current syntax or setup. Follow the documentation for the framework your project uses. [S1]

Follow-up question

Can you show a compact matrix and assertions without assuming my framework?

Geez

Here is a framework-neutral matrix. Assume loadItems calls an injected fetchItems function and returns its result. Replace the expected behavior with your own contract if, for example, your function converts errors into a status object.

  1. Success: dependency resolves to [{ id: 'a' }]; assert loadItems() resolves to that item.
  2. Rejection: dependency rejects with Error('offline'); assert loadItems() rejects with the expected error, or with the documented transformed result.
  3. Empty result: dependency resolves to []; assert loadItems() resolves to [] if empty collections are passed through.

Example test logic, expressed as pseudocode rather than framework-specific assertion syntax:

stub fetchItems to resolve with [{ id: 'a' }]
assert awaiting loadItems() equals [{ id: 'a' }]

stub fetchItems to reject with Error('offline')
assert awaiting loadItems() rejects with the expected error

stub fetchItems to resolve with []
assert awaiting loadItems() equals []

Make each test set its own stub behavior. That keeps one case’s setup from silently determining another case’s result. The assertions above describe intent; they are not copy-paste syntax for a particular runner.

Follow-up question

What does this look like in a small worked example?

Geez

Suppose the function simply delegates and returns the result. These are illustrative assumptions, not a claim about any particular project:

async function loadItems(fetchItems) {
  return await fetchItems()
}

For the success case, make the stub return [{ id: 'a' }], then check that awaiting loadItems(stub) gives the same value. For rejection, make the stub reject with Error('offline'), then check that awaiting the call rejects as expected. For empty data, make it resolve to [], then check that the result is an empty array—not an error—if that is the intended contract.

If your real function catches errors, changes the result shape, or retries, test that behavior instead of assuming this simple example applies. The source supports controlled stubs and edge-case testing, but does not establish your function’s intended contract or provide framework-specific assertion code. [S1]

Follow-up question

How do I make the cases useful for my own function?

Geez

Use the function’s public behavior as the target: what value does a caller receive, and how is a failure represented? Keep the fixture small and artificial, such as an item with a placeholder ID, and avoid personal data. Add a fourth case only when it checks a distinct promised behavior—for example, a rejected dependency that your function is supposed to convert into a fallback. Don’t add cases merely to duplicate the same outcome.

Before adopting a framework’s APIs, confirm its current setup and assertion syntax in that framework’s documentation. The cited Microsoft guidance covers several testing approaches, but it is Azure SDK integration-testing guidance, not a complete framework-independent manual. [S1]

A practical checklist

  • Write down the expected public result for success, rejection, and empty data.

  • Use a local stub with a small, controlled fixture instead of live credentials or personal data.

  • Give each test its own dependency behavior and assert the awaited result or rejection.

  • Treat an empty result separately from a rejected operation.

  • Use the setup and assertion syntax documented for your project’s test framework.

A few more useful answers

Should an empty result count as a failure?

Only if that matches the function’s intended contract. Test an empty resolved value separately so it is not confused with a rejected operation.

Do I need a mock, or is a stub enough?

A stub is enough when you need to supply controlled results. A mock may be useful when the test also needs to record calls. [S1]

Can I copy the example assertions directly into my test runner?

No: the example states the assertion intent in framework-neutral pseudocode. Check your chosen runner’s current setup and syntax; the cited guidance discusses Node.js’s built-in runner, Jest, and Vitest. [S1]

Sources

References used for this guide.

  1. [S1] Microsoft guidance on JavaScript SDK tests and test doubles
    learn.microsoft.com · Checked

YOUR QUESTION. YOUR DETAILS.

Let’s work through your situation.

Get a tailored answer or help with your next task. Starts at $1, with a quote before you pay.

Ask your own question
← Back to Geez at Work