Jest测试AppRouter组件报ReferenceError: Worker is not defined
Reference Error: Worker is not defined 问题描述
使用Jest与React Testing-Library测试React/Typescript组件,执行简单渲染测试时抛出Reference Error: Worker is not defined报错,不清楚测试场景下Worker被调用的原因,也不知道如何处理该报错。
测试用例代码
import {render, screen} from '@testing-library/react' import userEvent from '@testing-library/user-event' import React from 'react' import {Router} from 'react-router-dom' import AppRouter from '../router' test('AppRouter renders all routes and I can navigate to those pages', () => { render(<AppRouter />) 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> <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
调用栈截图

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)
初步排查
初步怀疑问题和MobX状态管理有关,测试时没有将组件包裹在对应Provider中,但不确定具体成因和修复方式。检索到的相关解决方案多为发布时间较早的StackOverflow内容,已知jest-worker包存在,但不确定运行这个基础渲染测试为何需要用到该包,也不清楚该如何配置使用。
报错根因
这个报错和jest-worker包没有任何关系。Jest测试默认运行在jsdom环境中,jsdom本身没有实现原生Web Worker API,当依赖链中存在调用new Worker()的逻辑时就会抛出该错误。从代码依赖判断,触发Worker调用的大概率是项目引入的Unirep相关SDK——这类链上/加密计算类库通常会用Web Worker处理耗时计算避免阻塞主线程。
另外MobX Provider缺失的问题确实存在,但这类问题只会触发context取值为undefined的报错,和Worker异常无关,初步排查方向有误。
修复方法
1. 为jsdom环境补充Worker mock
首先在Jest配置中指定setup文件,打开jest.config.js/jest.config.ts,添加如下配置:
module.exports = { // 保留原有其他配置 setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'], }
在项目src目录下新建/修改setupTests.ts,添加全局Worker的最小mock实现:
class MockWorker { onmessage: (payload: any) => void = () => {} constructor(public url: string) {} postMessage(message: any) { // 如果依赖需要Worker返回特定数据,可以在这里手动触发onmessage传值,基础渲染场景留空即可 this.onmessage({ data: null }) } terminate() {} } Object.defineProperty(window, 'Worker', { writable: true, value: MockWorker })
2. 补全测试中缺失的Context包裹
Header组件依赖UnirepContext、UserContext两个上下文,直接渲染AppRouter会因为上下文取值失败报错,修改测试用例,用对应Provider包裹组件并传入mock值:
import {render, screen} from '@testing-library/react' import React from 'react' import AppRouter from '../router' import { UnirepContext } from './context/Unirep' import UserContext from './context/User' test('AppRouter renders all routes and I can navigate to those pages', () => { // 按照业务逻辑填入测试需要的mock值即可 const mockUnirepConfig = { postReputation: 1 } const mockUserValue = { userState: false, netReputation: 0 } render( <UnirepContext.Provider value={mockUnirepConfig}> <UserContext.Provider value={mockUserValue}> <AppRouter /> </UserContext.Provider> </UnirepContext.Provider> ) screen.debug() })
3. 可选优化:mock无关第三方组件
如果react-favicon这类和页面核心逻辑无关的第三方组件仍触发环境兼容问题,可以直接在setup文件中mock掉,避免加载其内部实现:
// 添加到setupTests.ts中 jest.mock('react-favicon', () => () => null)
内容的提问来源于stack exchange,提问作者AAMCODE

