I’m currently trying to test my component that uses a custom hook, however I can’t seem to manually mock the custom hook used within the component.
My code is current as follow;
src/components/movies/MovieDetail.tsx
import { useParams } from 'react-router-dom';
import { Movie } from '../../models/Movie';
import { useFetch } from '../../hooks/useFetch';
import { SCYoutubeVideoPlayer } from '../YoutubeVideoPlayer';
import { SCTagList } from '../tags/TagList';
import { SCActorList } from '../actors/ActorList';
import { SCMovieSpecs } from './MovieSpecs';
import { SCServerError } from '../errors/ServerError';
import { SCLoading } from '../loading/Loading';
import styles from './MovieDetail.module.scss';
export const SCMovieDetail = () => {
const { slug } = useParams();
const { error, loading, data: movie } = useFetch<Movie>({ url: `http://localhost:3000/movies/${slug}` });
if (error) {
return <SCServerError error={error} />;
}
if (loading || movie === undefined) {
return <SCLoading />;
}
return (
<>
<section className={`${styles.spacing} ${styles.container}`}>
<h2>{movie.title}</h2>
<SCMovieSpecs movie={movie} />
</section>
<section className={styles['trailer-container']}>
<div>
<SCYoutubeVideoPlayer src={movie.trailer} />
</div>
</section>
<section className={`${styles.spacing} ${styles.container}`}>
<SCTagList tags={movie.tags} />
<div className={styles.description}>
<p>{movie.description}</p>
</div>
<SCActorList actors={movie.cast} />
</section>
</>
);
};
src/components/movies/MovieDetail.test.tsx
import renderer from 'react-test-renderer';
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { SCMovieDetail } from './MovieDetail';
describe('SCMovieDetail', () => {
const componentJSX = () => {
return (
<MemoryRouter>
<SCMovieDetail />
</MemoryRouter>
);
};
it('Should match snapshot', () => {
const component = renderer.create(<SCMovieDetail />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('Should render error', () => {
jest.mock('../../hooks/useFetch');
render(componentJSX());
expect(screen.getByText('Hello!')).toBeInTheDocument();
});
});
src/hooks/mocks/useFetch.ts
import { useFetchPayload, useFetchProps } from '../useFetch';
export const useFetch = <T>({ url, method = 'GET' }: useFetchProps): useFetchPayload<T> => {
let data: T | undefined = undefined;
let error: string | undefined = undefined;
let loading = false;
console.log('MOCKED!!!');
console.log('MOCKED!!!');
console.log('MOCKED!!!');
switch (url) {
case 'success':
data = {} as T;
break;
case 'error':
error = `${method} request failed`;
break;
case 'loading':
loading = true;
break;
}
return { data, error, loading };
};
src/hooks/useFetch.ts
import { useEffect, useState } from 'react';
export interface useFetchProps {
url: string;
method?: 'GET' | 'POST' | 'UPDATE' | 'PATCH' | 'DELETE';
}
export interface useFetchPayload<T> {
data?: T;
error?: string;
loading: boolean;
}
export const useFetch = <T>({ url, method = 'GET' }: useFetchProps): useFetchPayload<T> => {
const [data, setData] = useState<T | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
const [loading, setLoading] = useState(false);
console.log('REAL!!!');
console.log('REAL!!!');
console.log('REAL!!!');
useEffect(() => {
const abortController = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, { method, signal: abortController.signal });
if (!response || !response.ok) {
throw new Error('Request failed!');
}
const json = await response.json();
setData(json);
} catch (error: unknown) {
if (error instanceof DOMException && error.name == 'AbortError') {
return;
}
const customError = error as Error;
setError(customError.message);
} finally {
setLoading(false);
}
};
fetchData();
return () => abortController.abort();
}, [url, method]);
return { data, error, loading };
};
A quick overflow of the directory structure is;
src/
|-- ...
|-- components/
| | ...
| |-- movies/
| | |-- MovieDetail.tsx
| | |-- MovieDetail.test.tsx
| | |-- ...
|-- hooks/
| |-- __mocks__/
| | |-- useFetch.tsx
| |-- useFetch.tsx
| |-- ...
|-- ...
I’ve already searched multiple stackoverflow posts and other sites, but still no answer has been found. Hopefully one of you can help me finding the missing piece! I’m using React 18 with Jest 29. The goal is to use the least amount of node_modules as I’m still learning React and the react-testing-library in combination with Jest. Its also nice if the mock could be reused, so using the mocks directory is preferred over mocking the implementation directly in my test every single time.
2
Answers
It seemed that the fix was actually really simple. Instead of mocking inside the it scope, it should be entirely outside of the describe block.
Another solution I also found that grants additional flexibility is to be able to specify the return value inside the test callback like below. This solution does not use the mock file which is present in
__mocks__
but grants flexibility you'll otherwise not have.I prefer mock
fetch
rather thanuseFetch
hook.Because just mocking the return value of
useFetch
will destroy its functionality, internally used hook functions such asuseState
anduseEffect
have no chance to run. The component will be rendered multiple times due tosetState
with the real implementation of theuseFetch
hook.For the sake of simplicity, I will mock fetch directly instead of introducing msw package. Here is a simple example with minimal code to demonstrate:
MovieDetail.tsx
:hooks/useFetch.ts
:MovieDetail.test.tsx
:Test result: