使用React Testing Library测试Redux变更时的问题排查
我在测试Redux应用时遇到了困难,不确定是存在异步问题,还是对React Testing Library(RTL)的某些机制不够理解。之前用Enzyme处理这类测试毫无问题,但现在对RTL完全摸不着头绪。
要测试的是名为ItemBox的产品组件,包含产品文本和选择数量的下拉框,组件代码如下:
function ItemBox(props: Props) { const dispatch = useDispatch(); let selected = ""; if (props.product.amount && props.product.amount > 0) { selected = "selected"; } function handleChangeQuantity(e: any) { dispatch(createUpdateProductSelection({value: e.currentTarget.value, productid: props.product.id})); } return( <div className={"item-box " + selected}> <div className={"item-box-desc " + selected} title={props.product.name} > {props.product.name} </div> <div className={"item-box-bottom"} > <select className={"item-box-options"} id="qty" onChange={handleChangeQuantity} value={props.product.amount}> <option value="0">0</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> </div> </div> ) }
Reducer捕获action并更新产品数据的代码:
export function productsReducer(state: ProductsState, action: AnyAction): ProductsState { if (createUpdateProductSelection.match(action)) { let allProducts = state.allProducts.map((item): Product => { if (item.id === action.payload.productid) { return { ...item, amount: action.payload.value } } return item; }) return { ...state, allProducts: allProducts, }; } return state; }
实际应用运行正常,但测试时出现异常,测试代码如下:
let comboboxes: HTMLInputElement[] = await screen.findAllByRole("combobox"); fireEvent.change(comboboxes[0], {target: {value: "3"}}) comboboxes = await screen.findAllByRole("combobox"); expect(comboboxes[0].value).toEqual("3") console.info("" + screen.debug(comboboxes[0]))
测试通过了,因为检测到下拉框值为"3",但通过console.info查看HTML时,发现<select>标签下没有带selected属性的<option>,显示如下:
<select class="item-box-options" id="qty"> <option value="0">0</option> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> <option value="4">4</option> </select>
猜测可能是受控组件的问题?调试时发现handleChangeQuantity回调被正确触发,但Reducer代码还没执行,测试就走到了comboboxes = await screen.findAllByRole("combobox");这一行,意味着React更新逻辑未完成,测试就执行了下一行。想问是否正确处理了等待逻辑?是否有未察觉的异步操作?在我看来这是简单的Redux同步场景。
1. 核心问题:RTL等待逻辑的正确姿势
你当前的问题在于,虽然Redux的action是同步的,但React的状态更新是异步批处理的——即使同步dispatch action,React也不会立刻更新DOM,而是会把状态更新加入队列,在合适的时机批量执行。你用await screen.findAllByRole("combobox")的时候,这个查询可能在React完成DOM更新之前就返回了,因为findAllByRole等待的是元素出现,而不是元素更新后的状态。
2. 解决方法:等待Redux状态更新或DOM稳定
方式一:用waitFor等待断言条件满足
不要单纯等待元素存在,而是等待组件更新后的断言成立,确保React完成重渲染:
import { waitFor } from '@testing-library/react'; // 触发change事件 fireEvent.change(comboboxes[0], {target: {value: "3"}}); // 等待断言成立,确保React完成DOM更新 await waitFor(() => { expect(screen.getByRole("combobox").value).toBe("3"); }); // 此时再debug就能看到正确的selected属性了 screen.debug(screen.getByRole("combobox"));
方式二:直接检查Redux状态
也可以结合Redux store的状态来验证,确保状态更新后再检查DOM:
import { waitFor } from '@testing-library/react'; import { store } from '../your-store-path'; fireEvent.change(comboboxes[0], {target: {value: "3"}}); await waitFor(() => { const state = store.getState(); const targetProduct = state.products.allProducts.find(p => p.id === testProductId); expect(targetProduct?.amount).toBe("3"); }); // 此时DOM已同步更新,可正常查看selected属性 screen.debug(screen.getByRole("combobox"));
3. 为什么之前的测试通过了?
你之前的expect(comboboxes[0].value).toEqual("3")通过,是因为fireEvent.change会立刻修改DOM元素的value属性,但React还没来得及根据Redux的新状态重新渲染组件。这属于“DOM被事件修改,但组件状态还没同步”的中间状态,不是真正的组件更新后的状态,所以测试虽然通过,但实际DOM的selected属性还没更新。
4. 额外建议:用userEvent替代fireEvent
RTL推荐使用userEvent模拟真实用户交互,它更贴近真实行为,会自动处理部分异步逻辑:
import userEvent from '@testing-library/user-event'; // 替换fireEvent为userEvent await userEvent.selectOptions(screen.getByRole("combobox"), "3"); // 等待断言 await waitFor(() => { expect(screen.getByRole("combobox").value).toBe("3"); });
内容的提问来源于stack exchange,提问作者Oliver Watkins

