在 React 18 和 jest 29 中模拟自定义钩子

问题描述 投票:0回答:1

我目前正在尝试测试使用自定义钩子的组件,但是我似乎无法手动模拟组件中使用的自定义钩子。

我的代码是最新的,如下;

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 目录比每次在我的测试中直接模拟实现更好。

reactjs typescript jestjs mocking react-testing-library
1个回答
0
投票

我更喜欢模拟

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.
© www.soinside.com 2019 - 2024. All rights reserved.