Jest测试React/TS路由组件报JSON.parse Unexpected token u错误解决方法
问题背景
使用Jest搭配React Testing Library对React/TypeScript组件做单元测试时,抛出报错:SyntaxError: Unexpected token u in JSON at position 0 at JSON.parse
已知该错误通常由JSON.parse(undefined)触发,但测试场景下根因暂未定位:测试逻辑为跳转至/new路由渲染NewPage组件,组件执行到const state = JSON.parse(JSON.stringify(location.state))代码行时抛出上述错误,初步判断需要在Jest中mock相关对象,但不清楚具体实现方式。
现有测试用例代码
test('AppRouter should navigate to /new', () => { window.history.pushState({}, '', '/new') render( <BrowserRouter> <AppRouter /> </BrowserRouter> ) })
报错的NewPage组件代码
import { useEffect, useContext } from 'react' import { useHistory, useLocation } from 'react-router-dom' import { observer } from 'mobx-react-lite' import UserContext from '../../context/User' import PostContext from '../../context/Post' import WritingField from '../../components/writingField/writingField' import BasicPage from '../basicPage/basicPage' import { DataType } from '../../constants' const NewPage = () => { const history = useHistory() const location = useLocation<Location>() const state = JSON.parse(JSON.stringify(location.state)) const isConfirmed = state.isConfirmed const userContext = useContext(UserContext) const postContext = useContext(PostContext) useEffect(() => { console.log('Is this new page being confirmd? ' + isConfirmed) }, []) const preventPropagation = (event: any) => { event.stopPropagation() } const submit = async ( title: string, content: string, epkNonce: number, reputation: number ) => { if (!userContext.userState) { throw new Error('Should not be able to create post without login') } postContext.publishPost(title, content, epkNonce, reputation) history.push('/') } return ( <BasicPage> <h3>Create post</h3> <WritingField type={DataType.Post} submit={submit} submitBtnName="Post - 5 points" onClick={preventPropagation} /> </BasicPage> ) } export default observer(NewPage)
已尝试的无效方案
已尝试mock react-router-dom的useHistory、useLocation钩子,但仍抛出相同错误,mock代码如下:
const mockHistoryPush = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => ({ push: mockHistoryPush, }), useLocation: () => ({ pathname: "/" }) }));
补充说明:NewPage组件通过路由导航加载,该点不是问题诱因。
根因定位
报错本质是JSON.parse接收到了非字符串类型的入参undefined,具体触发原因有两个:
- 测试中直接调用
window.history.pushState({}, '', '/new')时,第一个参数传的是空对象,react-router读取路由state时拿到的就是undefined。JSON.stringify(undefined)的返回值本身是undefined类型而非字符串,传入JSON.parse后会被隐式转换为字符串"undefined",解析第一个字符u时就会抛出对应错误。 - 之前写的useLocation mock存在缺陷:返回值里没有定义
state字段,且mock的pathname写死为/,无法匹配/new路由;如果jest.mock没有放在测试文件顶层,mock甚至不会生效,组件还是会拿到真实的、不带state的location对象。
解决方案
方案1:使用React Router官方测试工具模拟路由(推荐)
不要手动mock useLocation,改用react-router-dom提供的MemoryRouter做测试,初始化时直接指定路由路径和对应的state,完全贴近真实运行场景,示例代码:
import { MemoryRouter } from 'react-router-dom'; import { render } from '@testing-library/react'; import AppRouter from './path/to/AppRouter'; test('AppRouter should navigate to /new', () => { render( <MemoryRouter initialEntries={[ { pathname: '/new', state: { isConfirmed: true } } ]}> <AppRouter /> </MemoryRouter> ) // 后续写断言逻辑即可 })
说明:initialEntries中定义的路由对象会完整作为location传给组件,state字段会被正确初始化,不会出现undefined的问题。
方案2:修正useLocation的mock逻辑
如果确实需要mock路由钩子,必须在mock返回值中补全组件用到的state字段,同时修正pathname匹配目标路由,示例代码:
const mockHistoryPush = jest.fn(); // jest.mock必须放在测试文件最顶层,import语句之后,不能写在test/describe块内部 jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: () => ({ push: mockHistoryPush, }), useLocation: () => ({ pathname: "/new", state: { isConfirmed: true // 字段和组件实际使用的state保持一致即可 } }) }));
组件侧鲁棒性优化
组件中直接对可能为undefined的location.state做JSON.parse本身存在线上风险,建议加默认值兜底,避免用户直接访问/new路由没带state时页面白屏:
// 原代码 // const state = JSON.parse(JSON.stringify(location.state)) // 优化后 const state = JSON.parse(JSON.stringify(location.state || { isConfirmed: false }))
内容的提问来源于stack exchange,提问作者AAMCODE

