测试接入MobX的React/TS组件报Worker未定义错误如何解决
问题描述
我正在使用Jest和React Testing-Library测试以MobX作为状态管理器的React/Typescript组件。
我了解RTL官方文档提供了创建custom render用于测试的方案,但不清楚在MobX场景下该如何实现,测试时需要为组件包裹对应的上下文包装层。
目前我仅尝试完成组件的基础渲染,运行测试时出现Reference Error: Worker is not defined报错,测试代码如下:
test('AppRouter renders all routes and I can navigate to those pages', () => { render( <AppRouter /> ) })
App Router 组件
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> <Favicon url={require('../public/favicon.ico')} /> <WebContext.Provider value={{ isMenuOpen, setIsMenuOpen, page, setPage, adminCode, setAdminCode, }} > <Header /> <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
我怀疑AppRouter渲染的Header组件是报错来源,因为Header组件依赖MobX store。
Header 组件
import { useContext, useState } from 'react' import { NavLink, useHistory, useLocation } from 'react-router-dom' import { observer } from 'mobx-react-lite' import { WebContext } from '../../context/WebContext' import UnirepContext from '../../context/Unirep' import UserContext from '../../context/User' const Header = () => { const history = useHistory() const location = useLocation() const { isMenuOpen, setIsMenuOpen } = useContext(WebContext) const [searchInput, setSearchInput] = useState<string>('') const unirepConfig = useContext(UnirepContext) const userContext = useContext(UserContext) const gotoNewPage = () => { if ( userContext.userState && userContext.netReputation >= unirepConfig.postReputation ) { history.push(`/new`, { isConfirmed: true }) } } const gotoUserPage = () => { history.push(`/user`, { isConfirmed: true }) } const openMenu = () => { if (!isMenuOpen) { console.log('open menu!') setIsMenuOpen(true) } } const handleSearchInput = (event: any) => { console.log('search input : ' + event.target.value) } return ( <header> <div className="navLinks"> <NavLink to="/" className="link" activeClassName="active" exact> <img src={require('../../../public/images/unirep-title.svg')} /> </NavLink> </div> {userContext.userState ? ( <div className="navButtons"> <div id="rep" onClick={gotoUserPage}> <img src={require('../../../public/images/lighting.svg')} /> {userContext.netReputation} </div> <div id="new" className={ location.pathname === '/new' ? 'navBtn chosen' : 'navBtn' } > <img src={require('../../../public/images/newpost.svg')} onClick={gotoNewPage} /> </div> <div id="user" className={ location.pathname === '/user' ? 'navBtn chosen' : 'navBtn' } > <img src={require('../../../public/images/user.svg')} onClick={gotoUserPage} /> </div> <div className="navBtn"> <img src={require('../../../public/images/menu.svg')} onClick={openMenu} /> </div> </div> ) : ( <div className="navButtons"> <div id="login" className="whiteButton" onClick={() => history.push('/login')} > Sign in </div> <div id="join" className="blackButton" onClick={() => history.push('/signup')} > Join </div> <div id="menu" className="navBtn"> <img src={require('../../../public/images/menu.svg')} onClick={openMenu} /> </div> </div> )} </header> ) } export default observer(Header)
解决方案
ReferenceError: Worker is not defined报错和MobX无直接关联:Jest运行在Node环境,不存在浏览器原生的Web Worker API,项目依赖(大概率是Unirep SDK或react-favicon)调用了Worker导致报错;同时当前测试未注入Header组件依赖的UnirepContext、UserContext,即使解决Worker问题也会出现上下文缺失错误,按以下步骤修复即可:
- 第一步:添加Worker全局Mock
在Jest测试初始化文件(CRA项目默认为src/setupTests.ts,自定义配置可在jest.config.js中指定setupFilesAfterStage字段对应的文件)添加如下Mock代码:
class MockWorker { constructor(url: string) { this.url = url } url: string onmessage: (payload: any) => void = () => {} postMessage(payload: any) { this.onmessage({ data: null }) } terminate() {} } (global as any).Worker = MockWorker
- 第二步:封装适配全量上下文的自定义render方法
参考RTL自定义渲染方案,将项目用到的所有Context(包括MobX相关的Context Provider)统一封装到测试渲染工具中,避免每个测试重复编写包裹层,代码示例:
// src/test-utils.tsx import { render as originalRender, RenderOptions } from '@testing-library/react' import { BrowserRouter } from 'react-router-dom' import { ReactElement, ReactNode } from 'react' import { WebContext } from './context/WebContext' import UnirepContext from './context/Unirep' import UserContext from './context/User' // 定义各Context的默认Mock值,可根据业务自行补充字段 const defaultWebContext = { isMenuOpen: false, setIsMenuOpen: jest.fn(), page: 'Home', setPage: jest.fn(), adminCode: '', setAdminCode: jest.fn() } const defaultUnirepConfig = { postReputation: 10 } const defaultUserContext = { userState: false, netReputation: 0 } interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> { webContext?: typeof defaultWebContext unirepConfig?: typeof defaultUnirepConfig userContext?: typeof defaultUserContext } const AllProviders = ({ children, webContext = defaultWebContext, unirepConfig = defaultUnirepConfig, userContext = defaultUserContext }: { children: ReactNode webContext?: typeof defaultWebContext unirepConfig?: typeof defaultUnirepConfig userContext?: typeof defaultUserContext }) => { return ( <BrowserRouter> <UnirepContext.Provider value={unirepConfig}> <UserContext.Provider value={userContext}> <WebContext.Provider value={webContext}> {children} </WebContext.Provider> </UserContext.Provider> </UnirepContext.Provider> </BrowserRouter> ) } export const render = ( ui: ReactElement, options?: CustomRenderOptions ) => { const { webContext, unirepConfig, userContext, ...rest } = options || {} return originalRender(ui, { wrapper: ({ children }) => ( <AllProviders webContext={webContext} unirepConfig={unirepConfig} userContext={userContext} > {children} </AllProviders> ), ...rest }) } // 导出RTL其余所有工具方法 export * from '@testing-library/react'
- 第三步:改造测试用例
测试文件中不再直接从@testing-library/react导入render,而是从刚才封装的test-utils导入,测试不同业务场景时可直接传入自定义的Context值覆盖默认Mock:
import { render, screen } from './test-utils' import AppRouter from './AppRouter' test('AppRouter renders all routes and I can navigate to those pages', () => { // 测试未登录场景,使用默认Mock值即可 render(<AppRouter />) expect(screen.getByText('Sign in')).toBeInTheDocument() }) test('render logged in state', () => { // 测试登录后场景,传入覆盖的用户上下文 render(<AppRouter />, { userContext: { userState: true, netReputation: 20 } }) expect(screen.getByText('20')).toBeInTheDocument() })
注意:你提供的原始组件代码中import关键字开头缺失了字母
i(写为mport),运行前请先修正该语法错误。
内容的提问来源于stack exchange,提问作者AAMCODE
相关产品推荐
相关产品推荐

