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',
});
})
})