React Testing Library+Vitest单元测试中如何正确更新输入框值?
输入框状态变更单元测试失败问题解决
问题背景
开发项目时,对输入框状态变更做单元测试,使用React Testing Library的userEvent.setup()模拟输入事件后,断言输入框值更新始终失败,输入框value始终为空。
原测试代码
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MockedProvider } from '@apollo/client/testing'; import CreateClientForm from '../../src/components/CreateClientForm'; import { vi } from 'vitest'; const mockProps: CreateClientFormProps = { clientName: '', setClientName: vi.fn(), clientEmail: '', setClientEmail: vi.fn(), clientPhone: '', setClientPhone: vi.fn(), }; beforeEach(() => { render( <MockedProvider> <CreateClientForm {...mockProps} /> </MockedProvider> ); }); describe('Create Client Form In Modal', () => { it('renders the correct initial input values', () => { expect(screen.getByLabelText('Client Name:')).toBeInTheDocument(); expect(screen.getByLabelText('Client Email:')).toBeInTheDocument(); expect(screen.getByLabelText('Client Phone:')).toBeInTheDocument(); }); it('updates state values as a user types', async () => { const user = userEvent.setup(); const nameInput = screen.getByRole('textbox', { name: 'Client Name:', }) as HTMLInputElement; // 检查初始值 expect(nameInput.value).toEqual(''); // 模拟用户输入 await user.type(nameInput, 'Johnny Bravo'); console.log( screen.getByRole('textbox', { name: 'Client Name:', }) ); // 断言更新后的值(此处失败) //expect(nameInput.value).toEqual('Johnny Bravo'); }); });
相关组件代码
CreateClientForm组件
import LabelWithInput from './common/LabelWithInput'; const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); }; const CreateClientForm = ({ clientName, setClientName, clientEmail, setClientEmail, clientPhone, setClientPhone, }: CreateClientFormProps) => { return ( <form onSubmit={handleSubmit}> <LabelWithInput label='Client Name:' type='text' id='clientName' value={clientName} onChange={(e) => setClientName(e.target.value)} className='form-control mb-2' data-testid='client-name' /> <LabelWithInput label='Client Email:' type='text' id='clientEmail' value={clientEmail} onChange={(e) => setClientEmail(e.target.value)} className='form-control mb-2' data-testid='client-email' /> <LabelWithInput label='Client Phone:' type='text' id='clientPhone' value={clientPhone} onChange={(e) => setClientPhone(e.target.value)} className='form-control mb-2' data-testid='client-phone' /> <button className='btn btn-primary'>Submit</button> </form> ); }; export default CreateClientForm;
LabelWithInput组件
import { type ComponentPropsWithoutRef } from 'react'; type LabelWithInputProps = { id: string; label: string; } & ComponentPropsWithoutRef<'input'>; const LabelWithInput = ({ id, label, ...props }: LabelWithInputProps) => { return ( <div> <label htmlFor={id}>{label}</label> <input id={id} {...props} /> </div> ); }; export default LabelWithInput;
终端错误信息
FAIL tests/components/CreateClientForm.test.tsx > Create Client Form In Modal > updates state values as a user types AssertionError: expected '' to deeply equal 'Johnny Bravo' - Expected + Received - Johnny Bravo ❯ tests/components/CreateClientForm.test.tsx:51:29 49| 50| // Verify updated value after change event 51| expect(nameInput.value).toEqual('Johnny Bravo'); | ^ 52| }); 53| });
依赖版本
"@testing-library/jest-dom": "^6.1.4", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.8", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@vitejs/plugin-react": "^4.0.3", "dom-testing-library": "^5.0.0", "eslint": "^8.45.0", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", "jsdom": "^22.1.0", "ts-node": "^10.9.1", "vite": "^4.4.5", "vitest": "^0.34.6"
问题原因
输入框是受控组件,value绑定的是props传入的clientName等状态值。原测试中使用vi.fn()模拟的setClientName等函数仅能记录调用情况,不会实际更新对应的状态值。因此即使触发了onChange事件,clientName始终为空,输入框值也不会变化,导致断言失败。
解决方案
创建一个模拟父组件,使用React的useState管理真实的状态,将状态和对应的setter函数传入CreateClientForm。这样触发输入事件时,setter会更新状态,输入框的value也会同步更新,断言即可通过。
最终测试代码
import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { MockedProvider } from '@apollo/client/testing'; import CreateClientForm from '../../src/components/CreateClientForm'; import { useState } from 'react'; // 创建模拟父组件管理状态 const MockParentComponent = () => { const [clientName, setClientName] = useState(''); const [clientEmail, setClientEmail] = useState(''); const [clientPhone, setClientPhone] = useState(''); return ( <MockedProvider> <CreateClientForm clientName={clientName} setClientName={setClientName} clientEmail={clientEmail} setClientEmail={setClientEmail} clientPhone={clientPhone} setClientPhone={setClientPhone} /> </MockedProvider> ); }; beforeEach(() => { render( <MockedProvider> <MockParentComponent /> </MockedProvider> ); }); describe('Create Client Form In Modal', () => { it('renders the correct initial input values', () => { expect(screen.getByLabelText('Client Name:')).toBeInTheDocument(); expect(screen.getByLabelText('Client Email:')).toBeInTheDocument(); expect(screen.getByLabelText('Client Phone:')).toBeInTheDocument(); }); it('updates state values as a user types', async () => { const user = userEvent.setup(); const nameInput = screen.getByRole('textbox', { name: 'Client Name:', }) as HTMLInputElement; // 检查初始值 expect(nameInput.value).toEqual(''); // 模拟用户输入 await user.type(nameInput, 'Johnny Bravo'); console.log( screen.getByRole('textbox', { name: 'Client Name:', }) ); // 断言更新后的值 expect(nameInput.value).toEqual('Johnny Bravo'); }); });
内容的提问来源于stack exchange,提问作者CodeConnoisseur
相关产品推荐
相关产品推荐

