React Testing Library测试Context时expect报innerHTML为null错误
问题修复方案
报错原因
- 测试用例共享渲染实例:你把
render方法放在了describe块的顶层,所有测试用例共用同一个组件渲染实例,容易出现状态污染,也不符合单测隔离的原则。 - 未等待异步更新:React的状态更新是异步执行的,你点击添加按钮触发Context状态更新后,同步执行查询时组件还没完成重渲染,目标元素还不存在,
queryByTitle会返回null,直接访问innerHTML属性就会抛出类型错误。 - 无空值判断:
queryBy*系列查询的特性就是找不到元素时返回null,直接访问属性前没有做空值校验也会触发报错。
如果排查完以上问题还是报错,可以确认CartContextProvider的addProduct逻辑是否正确,是否将传入的商品名赋值给了商品对象的itemId字段,避免title属性不匹配导致查询失败。另外原代码中列表渲染没有加key属性,也建议补充避免控制台警告。
修复方法
- 将渲染逻辑移动到每个测试用例内部,或者用
beforeEach包裹,保证每个测试用例的运行环境独立。 - 用异步查询方法
findBy*替代同步查询,等待元素渲染完成后再执行断言,也可以用waitFor包裹断言逻辑。 - 推荐用React Testing Library官方推荐的
screen对象获取查询方法,减少变量解构带来的错误。
修正后测试代码示例
class CartContextTest extends React.Component { constructor(props) { super(props); this.state={ productName: '', priceInUSD: 0, priceInEUR: 0, attribute: '' } } render() { return ( <CartContextProvider> <CartContext.Consumer> {context=>{ const {productsToPurchase, totalPrice, addProduct, decreaseProduct} = context return(<> <input type='text' title='product-name' onChange={ (e)=>{this.setState({productName: e.target.value})} } /> <input type='text' title='price-in-usd' onChange={ (e)=>{this.setState({priceInUSD: e.target.value})} } /> <input type='text' title='price-in-eur' onChange={ (e)=>{this.setState({priceInEUR: e.target.value})} } /> <input type='text' title='attribute' onChange={ (e)=>{this.setState({attribute: e.target.value})} } /> <button title='add-product' onClick={()=>{ addProduct(this.state.productName,[{currency: 'USD', amount: this.state.priceInUSD},{currency: 'EUR', amount: this.state.priceInEUR}] , this.state.attribute) }}>Add</button> {productsToPurchase.map(product=>{ return(<div key={product.itemId}> <h1 title={"Name " + product.itemId}>{product.itemId}</h1> <button title={"Plus " + product.itemId} onClick={()=>{addProduct(product.itemId, product.price, product.attributes)}}>+</button> <h1 title={"qty " + product.itemId}>{product.qty}</h1> <button title={"Minus " + product.itemId} onClick={()=>{decreaseProduct(product.itemId, product.attributes)}}>-</button> <>{product.price.map(price=>{ return(<h1 key={price.currency + " " + product.itemId} title={price.currency + " " + product.itemId}>{price.amount}</h1>) })}</> <h1>{product.attributes}</h1> </div>) })} </>) }} </CartContext.Consumer> </CartContextProvider> ); } } describe("Testing the ability to add and remove products from the cart", ()=>{ // 每个测试用例执行前重新渲染组件,保证状态独立 beforeEach(() => { render(<CartContextTest/>) }) it('All the starter components should be rendered', ()=> { expect(screen.queryByTitle("product-name")).toBeTruthy() expect(screen.queryByTitle("price-in-usd")).toBeTruthy() expect(screen.queryByTitle("price-in-eur")).toBeTruthy() expect(screen.queryByTitle("attribute")).toBeTruthy() expect(screen.queryByTitle("add-product")).toBeTruthy() }) it('Should add and remove products successfully', async ()=>{ // 添加iPhone XR fireEvent.change(screen.queryByTitle("product-name"), {target:{value: "iphone-xr"}}) fireEvent.change(screen.queryByTitle("price-in-usd"), {target:{value: "600"}}) fireEvent.change(screen.queryByTitle("price-in-eur"), {target:{value: "500"}}) fireEvent.change(screen.queryByTitle("attribute"), {target:{value: "blue"}}) fireEvent.click(screen.queryByTitle("add-product")) // 异步等待目标元素渲染完成 const productTitle = await screen.findByTitle("Name iphone-xr") expect(productTitle.innerHTML).toBe("iphone-xr") }) })
内容的提问来源于stack exchange,提问作者asem shaat
相关产品推荐
相关产品推荐

