我目前正在尝试测试使用自定义钩子的组件,但是我似乎无法手动模拟组件中使用的自定义钩子。
我的代码是最新的,如下;
src/组件/电影/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 };
};
目录结构的快速溢出是;
src/
|-- ...
|-- components/
| | ...
| |-- movies/
| | |-- MovieDetail.tsx
| | |-- MovieDetail.test.tsx
| | |-- ...
|-- hooks/
| |-- __mocks__/
| | |-- useFetch.tsx
| |-- useFetch.tsx
| |-- ...
|-- ...
我已经搜索了多个 stackoverflow 帖子和其他网站,但仍然没有找到答案。希望你们中的一个人能帮我找到丢失的那一块!我正在使用 React 18 和 Jest 29。目标是使用最少数量的 node_modules,因为我仍在学习 React 以及与 Jest 结合的反应测试库。如果模拟可以重用,那也很好,所以使用 mocks 目录比每次在我的测试中直接模拟实现更好。
我更喜欢模拟
fetch
而不是 useFetch
钩子。
因为仅仅模拟
useFetch
的返回值就会破坏它的功能,所以内部使用的钩子函数如useState
和useEffect
没有机会运行。
为了简单起见,我将直接模拟 fetch,而不是引入 msw 包。这是一个简单的例子,用最少的代码来演示:
MovieDetail.tsx
:
import React from 'react';
import { useFetch } from './hooks/useFetch';
export const SCMovieDetail = () => {
const movieQuery = useFetch<string>({ url: `http://localhost:3000/movies/1` });
console.log('🚀 ~ SCMovieDetail ~ movieQuery:', movieQuery);
return (
<>
<section>{movieQuery.data}</section>
</>
);
};
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 () => {
setLoading(true);
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 };
};
MovieDetail.test.tsx
:
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { SCMovieDetail } from './MovieDetail';
describe('SCMovieDetail', () => {
it('Should render data', async () => {
jest.spyOn(global, 'fetch').mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue('Hello!'),
} as unknown as Response);
render(<SCMovieDetail />);
expect(await screen.findByText('Hello!')).toBeInTheDocument();
});
});
测试结果:
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:19:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:20:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:21:11)
console.log
🚀 ~ SCMovieDetail ~ movieQuery: { data: undefined, error: undefined, loading: false }
at log (stackoverflow/78474280/MovieDetail.tsx:9:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:19:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:20:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:21:11)
console.log
🚀 ~ SCMovieDetail ~ movieQuery: { data: undefined, error: undefined, loading: true }
at log (stackoverflow/78474280/MovieDetail.tsx:9:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:19:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:20:11)
console.log
REAL!!!
at log (stackoverflow/78474280/hooks/useFetch.ts:21:11)
console.log
🚀 ~ SCMovieDetail ~ movieQuery: { data: 'Hello!', error: undefined, loading: false }
at log (stackoverflow/78474280/MovieDetail.tsx:9:11)
PASS stackoverflow/78474280/MovieDetail.test.tsx
SCMovieDetail
√ Should render error (98 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.183 s
Ran all test suites related to changed files.