Jest 测试由于初始空值而失败

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

我正在 NextJS 中为我的网站上的页面准备测试,在该页面中我应该通过钩子对 /api 路由进行 fetch 调用。该路由执行一个从 SQLite 数据库获取数据的查询,然后将其返回到页面以使用数据填充该页面。我的页面如下所示:

'use client';

import { useParams } from 'next/navigation';
import AssetButton from '@/components/AssetButton';
import AssetInfo from '@/components/AssetInfo';
import AssetLogo from '@/components/AssetLogo';
import AssetScreenshot from '@/components/AssetScreenshot';
import Details from '@/components/Details';
import Loader from '@/components/Loader';
import Page from '@/components/Page';
import Text from '@/components/Text';
import useFetchData from '@/hooks/useFetchData';
import { Asset } from '@/utils/types';

export default function ProgramDetails() {
  const params = useParams();
  const { id } = params;

  const {
    data: program,
    loading,
    error,
  } = useFetchData<Asset>(`/api/programs/${id}`);

  const errorData = <p>{error}</p>;

  const isDownload = !program?.link?.includes('https://');

  return (
    <Page>
      {loading ? (
        <Loader />
      ) : error ? (
        errorData
      ) : (
        <>
          <AssetLogo icon={program!.icon} />
          <Text content={program!.name?.toUpperCase()} type="title" />
          <Text content={program!.description} type="body" />
          <AssetInfo>
            <AssetScreenshot file={program!.icon} />
            <Details details={program!.details?.split(';')} />
          </AssetInfo>
          <AssetButton isDownload={isDownload} link={program!.link} />
        </>
      )}
    </Page>
  );
}

我使用

useFetchData
钩子来判断 api 调用何时加载以及何时获取数据,这样我就可以在获取数据时渲染加载器。钩子看起来像这样:

import { useState, useEffect } from 'react';

const useFetchData = <T>(url: string) => {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      fetch(url)
        .then(async (res) => {
          const data = await res.json();

          if (data) {
            setData(data);
          } else {
            setError('An unknown error has occurred');
          }
        })
        .catch((err) => {
          setError(err.message);
        })
        .finally(() => {
          setLoading(false);
        });
    };
    fetchData();
  }, [url]);

  return { data, loading, error };
};

export default useFetchData;

API 路由

/api/programs/[id]
看起来像这样:

import { NextResponse } from 'next/server';
import openDb from '@/utils/db';

export async function GET(
  request: Request,
  { params }: { params: { id: string } },
) {
  const db = openDb();
  const { id } = params;
  const program = db.prepare('SELECT * FROM programs WHERE id = ?').get(id);

  if (!program) {
    return NextResponse.json({ error: 'Program not found' }, { status: 404 });
  }

  return NextResponse.json(program);
}

因此测试旨在模拟 fetch 调用以返回虚拟数据并检查虚拟数据是否呈现:

import { useParams } from 'next/navigation';
import { render, screen, waitFor } from '@testing-library/react';
import ProgramDetails from '@/app/programs/[id]/page';
import { ViewportProvider } from '@/context/ViewportContext';

const mockProgramData = [
  {
    id: 1,
    name: 'Program 1',
    icon: 'icon1.png',
    description: 'Description for Program 1',
    details: ['detail 1', 'detail 2'],
    link: '/downloads/test.zip',
  },
];

jest.mock('next/navigation', () => ({
  useParams: jest.fn().mockReturnValue({ id: '1' }),
}));

describe('Program Details Page', () => {
  beforeEach(() => {
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve(mockProgramData),
      }),
    ) as jest.Mock;
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('renders the program title, description, and details correctly', async () => {
    render(
      <ViewportProvider>
        <ProgramDetails />
      </ViewportProvider>,
    );

    await waitFor(() => {
      expect(screen.queryByTestId('triple-spinner')).not.toBeInTheDocument();
    });

    const title = await screen.findByTestId('text-title');
    expect(title).toBeInTheDocument();
    expect(title).toHaveTextContent(mockProgramData[0].name);

    const body = await screen.findByTestId('text-body');
    expect(body).toBeInTheDocument();
    expect(body).toHaveTextContent(mockProgramData[0].description);

    await waitFor(() => {
      mockProgramData[0].details.forEach((currDetail) => {
        expect(screen.getByText(currDetail)).toBeInTheDocument();
        expect(screen.getByText(currDetail)).toBeInTheDocument();
      });
    });
  });
});

起初我假设,等待加载程序的测试 ID 不渲染就足以确保数据渲染。以防万一加载器看起来像这样:

import styles from './Loader.module.css';

const Loader: React.FC = () => {
  return (
    <div data-testid="loader" id={styles.loader}>
      <div
        className={styles['triple-spinner']}
        data-testid="triple-spinner"
      ></div>
    </div>
  );
};

export default Loader;

然后,我将通过测试 id 获取其他元素是否存在,并检查文本是否已渲染并匹配。具有这些 id 的文本组件是这样的:

import styles from './Text.module.css';

interface TextProps {
  content: string;
  type: 'body' | 'card' | 'hero' | 'modal' | 'title';
}

const Text: React.FC<TextProps> = ({ content, type }) => {
  return type === 'title' ? (
    <h3 className={styles[type]} data-testid="text-title">
      {content}
    </h3>
  ) : (
    <p className={styles[type]} data-testid="text-body">
      {content}
    </p>
  );
};

export default Text;

但是当我运行测试时,我收到此错误:

expect(element).toHaveTextContent()

    Expected element to have text content:
      Program 1
    Received:

      45 |     const title = await screen.findByTestId('text-title');
      46 |     expect(title).toBeInTheDocument();
    > 47 |     expect(title).toHaveTextContent(mockProgramData[0].name);
         |                   ^
      48 |
      49 |     const body = await screen.findByTestId('text-body');

由于没有任何显示,我尝试在钩子中打印 fetch 调用的结果,我看到了两次迭代:一次返回的数据为空,另一次返回测试数据。因此,我似乎需要能够跳过第一个调用,以确保我可以使用实际返回的数据,但我怎样才能实现这一点呢?

javascript reactjs next.js jestjs
1个回答
0
投票

正如 @hoangdv 提到的,解决这个问题的简单方法是更改模拟返回数据:

global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve(mockProgramData[0]),
      }),
    ) as jest.Mock;

相反,我正在返回

mockProgramData
,并且我没有意识到我从钩子的 fetch 调用中写入的日志正在返回一个数组: enter image description here 这就是问题所在,因为页面组件无法正确解构 Asset 元素,从而看起来像是接收初始渲染时 fetch 调用首先返回的未定义数据。

© www.soinside.com 2019 - 2024. All rights reserved.