React Testing Library解码JWT时出现Cannot read properties of undefined错误
问题根源分析
你的测试报错是因为jest.mock("utils/keycloak")将keycloak对象置为空mock,keycloak.token默认是undefined,导致parseJwt函数执行token.split('.')时抛出Cannot read properties of undefined错误。另外你的useEffect依赖数组存在问题:依赖userHasCommunities会导致每次调用setUserHasCommunities都触发useEffect,形成无限循环。
解决方案
步骤1:修复组件内的useEffect依赖
将useEffect的依赖数组改为空数组,确保仅在组件挂载时执行一次解码逻辑:
// Menu组件内的useEffect修改 useEffect(() => { function parseJwt (token) { if (!token) return {}; // 增加空值判断,避免token为undefined时报错 var base64Url = token.split('.')[1]; var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); if (JSON.parse(jsonPayload).communities?.length > 0){ setUserHasCommunities(true) } return JSON.parse(jsonPayload); }; parseJwt(keycloak.token); }, []) // 改为空数组,仅挂载时执行
步骤2:修改测试文件,Mock keycloak.token并覆盖场景
给mock的keycloak对象赋值有效JWT(可通过在线JWT生成器生成对应payload),覆盖两种用户场景:
import React from "react"; import {shallow} from "enzyme"; import {Menu} from "features/layouts/logged-in/menu/index"; import {historyObject} from "historyObject"; import { keycloak } from "utils/keycloak"; jest.mock("utils/keycloak"); jest.mock("historyObject") describe("Menu test", () => { const props = { t: jest.fn((key) => key), // 让翻译函数直接返回key,方便测试断言 }; beforeEach(() => { jest.resetAllMocks(); historyObject.location.pathname = ""; // 重置路由路径 }); // 测试无社区权限的用户:渲染3个菜单 it('renders menu without community entry for users without communities', () => { // Mock无communities字段的JWT keycloak.token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb21tdW5pdGllcyI6W10sImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; const wrapper = shallow(<Menu {...props} />); expect(wrapper.find('li')).toHaveLength(3); }); // 测试有社区权限的用户:渲染4个菜单 it('renders menu with community entry for users with communities', () => { // Mock含communities字段的JWT keycloak.token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb21tdW5pdGllcyI6WyJjb21tdW5pdHkxIl0sImlhdCI6MTUxNjIzOTAyMn0.0W99VdZ7aU5Q4Ae6zr6XaK5Q4Ae6zr6XaK5Q4Ae6zr6X"; const wrapper = shallow(<Menu {...props} />); expect(wrapper.find('li')).toHaveLength(4); const communityItem = wrapper.find('li').filterWhere(item => item.prop('onClick').toString().includes('/community')); expect(communityItem).toHaveLength(1); }); it('marks current path as selected', () => { historyObject.location.pathname = "/portfolio"; keycloak.token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb21tdW5pdGllcyI6W10sImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; const wrapper = shallow(<Menu {...props} />); expect(wrapper.find('.selected')).toHaveLength(1); }); it('triggers history push when menu item is clicked', () => { keycloak.token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb21tdW5pdGllcyI6W10sImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; const wrapper = shallow(<Menu {...props} />); wrapper.find('li').at(0).prop('onClick')(); expect(historyObject.push).toHaveBeenCalledWith("/portfolio"); }); });
进阶优化:抽离parseJwt到工具函数
将parseJwt抽离到单独的工具文件,方便单独测试和Mock:
- 创建
utils/jwt.js:
export function parseJwt(token) { if (!token) return {}; const base64Url = token.split('.')[1]; const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const jsonPayload = decodeURIComponent(window.atob(base64).split('').map(c => { return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); }).join('')); return JSON.parse(jsonPayload); }
- 组件内导入使用:
import { parseJwt } from "utils/jwt"; // useEffect内直接调用 useEffect(() => { const payload = parseJwt(keycloak.token); if (payload.communities?.length > 0) { setUserHasCommunities(true); } }, [])
- 测试时Mock工具函数:
import { parseJwt } from "utils/jwt"; jest.mock("utils/jwt", () => ({ parseJwt: jest.fn() })); // 在测试用例中控制返回值 it('renders community menu when user has communities', () => { parseJwt.mockReturnValue({ communities: ['test-community'] }); const wrapper = shallow(<Menu {...props} />); expect(wrapper.find('li')).toHaveLength(4); });
内容的提问来源于stack exchange,提问作者Asher
相关产品推荐
相关产品推荐

