Jest测试中React Router组件渲染不符合预期原因排查
React路由单元测试screen.debug输出DOM异常问题
问题描述
进行React路由组件单元测试时,组件渲染完成后调用测试用例内的screen.debug()方法,输出的DOM结构与预期不符。
相关代码
测试用例代码
import { render, userEvent as user, screen, getByRole } from '@testing-library/react' import { Router } from 'react-router-dom' import { createMemoryHistory } from 'history' import AppRouter from '../router' test('Renders AppRouter', () => { const history = createMemoryHistory({ initialEntries: ['/post'] }) render(() => ( <Router history={history}> <AppRouter /> </Router> )) screen.debug() })
被测AppRouter组件代码
import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom' import { useState } from 'react' import useLocalStorage from './hooks/useLocalStorage' import * as Constants from './constants' import Header from './layout/header/header' import MainPage from './pages/mainPage/mainPage' import PostPage from './pages/postPage/postPage' import UserPage from './pages/userPage/userPage' import LoginPage from './pages/loginPage/loginPage' import SignupPage from './pages/signupPage/signupPage' import NewPage from './pages/newPage/newPage' import FeedbackPage from './pages/feedbackPage/feedbackPage' import AdminPage from './pages/adminPage/adminPage' import SettingPage from './pages/settingPage/settingPage' import { WebContext } from './context/WebContext' import Favicon from 'react-favicon' const AppRouter = () => { const [adminCode, setAdminCode] = useLocalStorage('admin', '') const [isMenuOpen, setIsMenuOpen] = useState(false) const [page, setPage] = useState(Constants.Page.Home) return ( <BrowserRouter> <div role="hello"> <Favicon url={require('../public/favicon.ico')} /> <WebContext.Provider value={{ isMenuOpen, setIsMenuOpen, page, setPage, adminCode, setAdminCode, }} > <Header /> <h1> hello </h1> <Switch> <Route component={MainPage} path="/" exact={true} /> <Route component={PostPage} path="/post/:id" /> <Route component={UserPage} path="/user" /> <Route component={LoginPage} path="/login" /> <Route component={SignupPage} path="/signup" /> <Route component={NewPage} path="/new" /> <Route component={FeedbackPage} path="/feedback" /> <Route component={AdminPage} path="/admin" /> <Route component={SettingPage} path="/setting" /> <Route component={() => <Redirect to="/" />} /> </Switch> </WebContext.Provider> </div> </BrowserRouter> ) } export default AppRouter
相关运行截图
- 代码运行追踪截图:

- render不包裹函数时的报错截图:

- Favicon相关报错截图:

问题根因
一共4个问题叠加导致DOM输出异常:
render方法传参错误@testing-library/react的render第一个参数要求传入React Element,当前写法传了一个返回JSX的箭头函数,RTL不会执行这个函数渲染内部的路由和组件,自然拿不到预期DOM。去掉箭头函数直接传JSX时报错,是其他问题导致的,不是传参方式错误。- 路由实例嵌套冲突
测试代码里已经手动用Router+自定义memoryHistory包裹了AppRouter,但AppRouter内部最外层又套了一层BrowserRouter,两个独立的路由上下文嵌套会导致路由匹配完全失效。 - 第三方组件未做测试适配
测试运行在jsdom环境,没有真实浏览器的head标签操作能力,react-favicon组件会尝试修改页面头部图标链接,直接渲染会抛出运行时错误中断组件渲染。 - 路由规则不匹配
测试初始化的访问路径是/post,但路由配置里PostPage对应的路径是/post/:id,要求必须带动态id参数才能匹配,直接访问/post会命中最后一条兜底规则重定向到首页,也会导致渲染结果不符合预期。
修复步骤
- 修正render传参,去掉外层多余的箭头函数
错误写法:
render(() => ( <Router history={history}> <AppRouter /> </Router> ))
改为:
render( <Router history={history}> <AppRouter /> </Router> )
- 解决双层路由冲突
推荐把AppRouter内部的BrowserRouter抽离到项目入口文件(比如index.js)中,AppRouter只保留Switch、路由配置和全局上下文逻辑,这样测试时用自定义memoryHistory的Router包裹组件不会出现上下文冲突。如果暂时不想调整组件结构,也可以在测试时直接渲染AppRouter,不要手动在外层套Router,通过RTL的wrapper配置项统一处理路由包裹逻辑。 - Mock掉不兼容jsdom的Favicon组件
在测试文件顶部或者jest全局setup文件中添加mock代码,让测试环境下Favicon渲染为空:
jest.mock('react-favicon', () => () => null)
- 修正测试初始化的路由路径
如果要测试PostPage渲染逻辑,初始化路由需要带上必填的id参数,比如:
const history = createMemoryHistory({ initialEntries: ['/post/1'] })
以上修改完成后重新运行测试,screen.debug()即可正常输出预期的DOM结构。
内容的提问来源于stack exchange,提问作者AAMCODE
相关产品推荐
相关产品推荐

