Skip to content

Instantly share code, notes, and snippets.

@HendrikPetertje
Last active April 23, 2024 07:47
Show Gist options
  • Save HendrikPetertje/4f3131d09d3a45054f2e78988ca03298 to your computer and use it in GitHub Desktop.
Save HendrikPetertje/4f3131d09d3a45054f2e78988ca03298 to your computer and use it in GitHub Desktop.
The deferred promise helper

The deferred promise helper is a way to run promise functions in stages while in the test environment. doing so makes it easier to test intermediary states.

DeferedPromiseHelper.ts

class DeferredPromiseHelper<IResolve, IReject> {
  promise: Promise<IResolve>;

  reject: (reason?: IReject) => void = () => {
    throw new Error('reject not working');
  };

  resolve: (value: IResolve) => void = () => {
    throw new Error('resolve not working');
  };

  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.reject = reject;
      this.resolve = resolve;
    });
  }
}

export default DeferredHelper;

Now with this piece of code you can start to work with promises. Let's take the following React Hook as an example:

useArticles.ts

export default function useArticles(category: string) {
  const [articlesState, setArticlesState] = useState<'initial' | 'loading' | 'ready'>('initial')
  const [articles, setArticles] useState<Article[]>([])

  const isMounted = useIsMounted(); // https://usehooks-ts.com/react-hook/use-is-mounted

  useEffect(() => {
    setArticlesState('loading')
    setArticles([]);

    const loadArticles = async () => {
      const newArticles = await fetchArticles(category)
      if (!isMounted()) return;

      setArticles(newArticles)
      setArticlseState('ready')
    }

    loadArticles().catch(errorHandler);
  }, [category]);

  return {articles, articlesState}
}

In a test we would like to make sure that the articles are wiped and that the articleState is actually 'loading' while the data is loading. this is where our deferred hook comes in:

useArticles.unit.test.ts

import * as ArticleRepo from '~repos/article'
import DeferredPromiseHelper from '~helpers/testHelpers/DeferredPromiseHelper.ts'
import useArticles from './useArticles'


describe('useArticles', () => {
  it('should set articleState to "loading" prior to loading and "ready" after loading', async () => {
    const deferredPromise = new DeferedPromiseHelper<ArticleRepo.Article[], unknown>()
    vi.spyOn(ArticleRepo, 'fetchArticles').mockImplementation(() => deferredPromise.promise)

    let hookResult: MyHookResultType;
    await act(async () => {
      hookResult  = renderHook(() => useArticles('localNews'))
    });

    expect(hookResult!.result.current).toEqual({
      articles: [],
      articlesState: 'loading',
    });
    
    wait act(async () => {
      deferredPromise.resolve(articlesFixture);
    });

    expect(hookResult!.result.current).toEqual({
      articles: articlesFixture,
      articlesState: 'ready',
    });
  })  
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment