首页 > 解决方案 > 尽管 DOM 行为正确,但在 React 测试库/Jest 中测试失败

问题描述

我对 Jest 和测试还很陌生,所以我正在使用 React、React 测试库和 Jest 制作一个应用程序来提高我的技能。

我的一项测试失败了,我不知道为什么。这是我的测试代码:

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// using UrlShortener since the state has been lifted up for UrlList
import UrlShortener from '../../pages/UrlShortener/UrlShortener'

...

    test('URL list displays valid URL from input bar', async () => {
        const passingText = 'http://www.google.com';
        const testText = 'test4';
        render(<UrlShortener />);
        const urlInput = screen.getByPlaceholderText('Enter URL here...');
        const nameInput = screen.getByPlaceholderText('Name your URL...');
        const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
        userEvent.type(urlInput, passingText);
        userEvent.type(nameInput, testText);
        userEvent.click(submitBtn);
        const listButton = screen.getByText('Link History');
        userEvent.click(listButton);
        const list = await screen.findAllByText(/visits/i);
        await waitFor(() => expect(list).toHaveLength(4));
    });

让我感到困惑的是,我可以从失败的测试中看到该列表在日志中有 4 个元素长,但由于某种原因,它没有在 expect() 函数中被拾取。这是日志给我的内容(它清楚地显示了列表中的 4 个元素):

    expect(received).toHaveLength(expected)

    Expected length: 4
    Received length: 3
    Received array:  [<p>Visits: 2</p>, <p>Visits: 1</p>, <p>Visits: 5</p>]

...

<div>
    <div
    class="sc-iqHYmW gBcZyO"
    >
    <p>
      <a
        href="http://www.baseUrl.com/123"
      >
        test1
      </a>
    </p>
    <p>
      Visits: 
      2
    </p>
    </div>
    <div
    class="sc-iqHYmW gBcZyO"
    >
    <p>
      <a
        href="http://www.baseUrl.com/456"
      >
        test2
      </a>
    </p>
    <p>
      Visits: 
      1
    </p>
    </div>
    <div
    class="sc-iqHYmW gBcZyO"
    >
    <p>
      <a
        href="http://www.baseUrl.com/789"
      >
        test3
      </a>
    </p>
    <p>
      Visits: 
      5
    </p>
    </div>
    <div
    class="sc-iqHYmW gBcZyO"
    >
    <p>
      <a
        href="http://www.baseUrl.com/shorten/123"
      >
        test4
      </a>
    </p>
    <p>
      Visits: 
      9
    </p>
    </div>
</div>

DOM 怎么可能在日志中按预期运行,但在实际测试中却失败了?

更新:

我正在添加更多信息,所以很明显我在做什么。基本上,我已经将状态从子组件 ( UrlList) 提升到父组件 (),UrlShortener以便我可以将状态更新器函数向下传递给兄弟组件 ( UrlBar)。UrlShortener 对后端进行 axios 调用,然后将 URL 列表传递给UrlList组件。当您单击UrlBar组件中的提交按钮时,它会重新运行 axios 调用并使用添加的新 URL 更新列表。

父组件:

import { useEffect, useState } from 'react';
import { SectionPage, BackButton, PageTitle } from './style';
import axios from 'axios';
import UrlBar from '../../components/UrlBar/UrlBar';
import UrlList from '../../components/UrlList/UrlList';
import { Url } from '../../types/types';

const UrlShortener = () => {
    const [urls, setUrls] = useState<Url[] | []>([]);

    const getUrls = () => {
        axios
            .get('https://fullstack-demos.herokuapp.com/shorten/urls/all')
            .then((res) => setUrls(res.data));
    };

    useEffect(() => {
        getUrls();
    }, []);

    return (
        <SectionPage>
            <BackButton href='/'>Go Back</BackButton>
            <PageTitle>URL Shortener</PageTitle>
            <UrlBar getUrls={getUrls} />
            <UrlList urls={urls} />
        </SectionPage>
    );
};

export default UrlShortener;

孩子们:

import React, { useState } from 'react';
import {
    ComponentWrapper,
    Subtitle,
    Triangle,
    LinksContainer,
    LinkGroup,
} from './style';
import { Url } from '../../types/types';

interface IProps {
    urls: Url[] | [];
}

const UrlList: React.FC<IProps> = ({ urls }) => {
    const [open, setOpen] = useState(false);

    const handleClick = () => {
        setOpen((prevState) => !prevState);
    };

    return (
        <ComponentWrapper>
            <Subtitle onClick={handleClick}>
                Link History <Triangle>{open ? '▼' : '▲'}</Triangle>
            </Subtitle>
            <LinksContainer>
                <div>
                    {open &&
                        urls.map(({ urlId, shortUrl, urlName, visits }: Url) => (
                            <LinkGroup key={urlId}>
                                <p>
                                    <a href={shortUrl}>{urlName}</a>
                                </p>
                                <p>Visits: {visits}</p>
                            </LinkGroup>
                        ))}
                </div>
            </LinksContainer>
        </ComponentWrapper>
    );
};

export default UrlList;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { UrlInput, NameInput, UrlButton } from './style';
import { validateUrl } from '../../utils/utils';

interface IProps {
    getUrls: () => void;
}

const UrlBar: React.FC<IProps> = ({ getUrls }) => {
    const [urlInput, setUrlInput] = useState('');
    const [nameInput, setNameInput] = useState('');
    const [error, setError] = useState<boolean | string>(false);

    useEffect(() => {
        // Cleanup fixes React testing error: "Can't perform a React state update on an unmounted component"
        return () => {
            setUrlInput('');
        };
    }, []);

    const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setUrlInput(e.target.value);
    };

    const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setNameInput(e.target.value);
    };

    const handleSubmit = async (e: React.SyntheticEvent) => {
        e.preventDefault();
        if (!nameInput) {
            setError('Please name your URL');
        } else if (!validateUrl(urlInput)) {
            setError('Invalid Input');
        } else {
            setError(false);
            await axios.post('https://fullstack-demos.herokuapp.com/shorten', {
                longUrl: urlInput,
                urlName: nameInput,
            });
            setUrlInput('');
            setNameInput('');
            getUrls();
        }
    };

    return (
        <div>
            <form onSubmit={handleSubmit}>
                <NameInput
                    type='text'
                    name='nameInput'
                    id='nameInput'
                    placeholder='Name your URL...'
                    maxLength={20}
                    onChange={handleNameChange}
                    value={nameInput}
                />
                <UrlInput
                    type='text'
                    name='urlInput'
                    id='urlInput'
                    placeholder='Enter URL here...'
                    onChange={handleUrlChange}
                    value={urlInput}
                />
                <UrlButton name='button' type='submit'>
                    Shorten!
                </UrlButton>
                {error && <label htmlFor='urlInput'>{error}</label>}
            </form>
        </div>
    );
};

export default UrlBar;

标签: reactjsjestjsreact-testing-library

解决方案


所以在努力让我的测试通过另一个组件之后,我终于想出了如何让这个通过。显然我只需要添加更多的 waitFor() 和 await 语句来捕获我的组件中发生的一些异步内容。如果我说我理解为什么这可以解决我的问题,那我是在撒谎,但现在我知道,如果我的测试失败,即使我可以在 JEST DOM 中看到正确的结果,这可能与缺少 waitFor / awaits 有关。

    test('URL list displays valid URL from input bar', async () => {
        const passingText = 'http://www.google.com';
        const testText = 'test4';
        render(<UrlShortener />);
        const urlInput = screen.getByPlaceholderText('Enter URL here...');
        const nameInput = screen.getByPlaceholderText('Name your URL...');
        const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
        userEvent.type(urlInput, passingText);
        userEvent.type(nameInput, testText);
        await waitFor(() => userEvent.click(submitBtn));
        const listButton = await screen.findByText('Link History');
        await waitFor(() => userEvent.click(listButton));
        const list = await screen.findAllByText(/visits/i);
        await waitFor(() => expect(list).toHaveLength(4));
    });
});

推荐阅读