Skip to content
easy-cancelable-promise

Testing

Test cancelable code with fake timers, hand the code under test a promise you control, and assert on the status instead of on timing.

GuideTypeScriptTry an example

Nothing to mock

A CancelablePromise is a real promise with no dependencies. It runs the same in Node, jsdom and the browser, so tests use the real class.

Test that canceling stops the work

The claim of a cancelable function is that its work stops. Assert on that, not only on the rejection.

delay.ts
import { CancelablePromise } from 'easy-cancelable-promise';

/** Resolves with the value after the delay. Canceling it clears the timer. */
export const delay = <T>(value: T, milliseconds: number) =>
  new CancelablePromise<T>((resolve, _reject, { onCancel }) => {
    const timer = setTimeout(() => resolve(value), milliseconds);

    onCancel(() => clearTimeout(timer));
  });
delay.test.ts
import { delay } from './delay';

describe('delay', () => {
  beforeEach(() => vi.useFakeTimers());
  afterEach(() => vi.useRealTimers());

  it('resolves with the value after the delay', async () => {
    const task = delay('done', 1000);

    expect(task.status).toBe('pending');

    await vi.advanceTimersByTimeAsync(1000);

    expect(await task).toBe('done');
    expect(task.status).toBe('resolved');
  });

  it('stops the timer when it is canceled', async () => {
    const task = delay('done', 1000);

    task.cancel('Not needed');

    // the status is known at once, no need to wait for the rejection
    expect(task.status).toBe('canceled');
    expect(vi.getTimerCount()).toBe(0);

    await expect(task).rejects.toBe('Not needed');
  });
});
  • status changes as soon as cancel is called, so it can be asserted without awaiting anything.
  • With fake timers, getTimerCount shows that the timer was cleared.
  • The rejection carries the reason that was passed to cancel.

Hand the code a promise you control

Code that receives a promise is tested by giving it one. defer creates a promise the test resolves, rejects, cancels or reports progress on, at the moment it chooses.

deferred.test.ts
import { defer } from 'easy-cancelable-promise';
import type { CancelablePromise } from 'easy-cancelable-promise';

/** The code under test receives the promise, so a test can hand it one it controls. */
const describeRequest = async (request: CancelablePromise<string>) => {
  try {
    return `Loaded ${await request}`;
  } catch {
    return request.status === 'canceled' ? 'Canceled' : 'Failed';
  }
};

describe('describeRequest', () => {
  it('describes each way a request can end', async () => {
    const loaded = defer<string>();
    const failed = defer<string>();
    const canceled = defer<string>();

    loaded.resolve('user');
    failed.reject(new Error('500'));
    canceled.cancel();

    expect(await describeRequest(loaded.promise)).toBe('Loaded user');
    expect(await describeRequest(failed.promise)).toBe('Failed');
    expect(await describeRequest(canceled.promise)).toBe('Canceled');
  });

  it('receives the progress the test reports', () => {
    const request = defer<string>();
    const onProgress = vi.fn();

    request.promise.onProgress(onProgress);
    request.reportProgress(40, { loaded: 4 });

    expect(onProgress).toHaveBeenCalledWith(40, { loaded: 4 });
  });
});

Next

Documentation
↑↓ Navigate ↵ Open Esc Closeeasy-cancelable-promise docs
Get started

Promises you can actually control.