React Context 和 Hooks API 的酶错误
Posted
技术标签:
【中文标题】React Context 和 Hooks API 的酶错误【英文标题】:Enzyme errors with React Context and Hooks API 【发布时间】:2019-07-25 06:10:41 【问题描述】:我创建了这个 RootContext 来处理我的小型 React Hooks 应用程序的身份验证。除了使用 Enzyme 的 shallow
和 mount
出现奇怪的错误外,一切都按预期工作。
我正在尝试这样测试:
const wrapper = mount(<Login />)
索引:
import RootContext from './RootContext'
function Root()
return (
<RootContext>
<App />
</RootContext>
)
ReactDOM.render(<Root/>, document.getElementById('root'));
根上下文:
import React, useEffect, useState from 'react'
export const RootContext = React.createContext()
export default ( children ) =>
const auth = window.localStorage.getItem('authenticated') || 'false'
const cred = window.localStorage.getItem('credentials') || null
const [authenticated, setAuthenticated] = useState(auth)
const [credentials, setCredentials] = useState(cred)
useEffect(
() =>
window.localStorage.setItem('authenticated', authenticated)
window.localStorage.setItem('credentials', credentials)
,
[authenticated, credentials]
)
const defaultContext =
authenticated,
setAuthenticated,
credentials,
setCredentials
return (
<RootContext.Provider value=defaultContext>
children
</RootContext.Provider>
)
登录、注销和注册都使用了导致此问题的useAuthenticate
挂钩。 BmiForm 组件工作正常。
import AuthenticatedRoute from './AuthenticatedRoute'
export default function App()
return (
<Router>
<Header />
<Switch>
<Container>
<Row>
<Col md= span: 4, offset: 4 >
<AuthenticatedRoute exact path="/" component=BmiForm />
<Route exact path="/login" component= Login />
<Route exact path="/logout" component= Logout />
<Route exact path="/register" component= Register />
</Col>
</Row>
</Container>
</Switch>
</Router>
)
导致问题的useAuthenticate
钩子:
import useReactRouter from 'use-react-router';
import RootContext from './../RootContext'
export default function useAuthenticate()
const history = useReactRouter()
const
authenticated,
setAuthenticated,
credentials,
setCredentials
= useContext(RootContext);
将useAuthenticate
钩子添加到 BmiForm 会导致其测试以同样的方式失败。
import useAuthenticate from './custom/useAuthenticate'
export default function BmiForm(props)
const credentials, setAuthenticated = useAuthenticate()
我得到的第一个错误:
TypeError: Cannot read property 'authenticated' of undefined
5 | export default function useAuthenticate()
6 | const
> 7 | authenticated,
| ^
8 | setAuthenticated,
9 | credentials,
10 | setCredentials
stacktrace 的第二个错误:
use-react-router may only be used within a react-router context.
4 |
5 | export default function useAuthenticate()
> 6 | const history = useReactRouter()
| ^
7 | const
8 | authenticated,
9 | setAuthenticated,
at useRouter (node_modules/use-react-router/src/use-react-router.ts:20:11)
at useAuthenticate (src/custom/useAuthenticate.js:6:23)
at BmiForm (src/BmiForm.js:15:45)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:12839:18)
at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:14816:13)
at beginWork (node_modules/react-dom/cjs/react-dom.development.js:15421:16)
at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:19108:12)
at workLoop (node_modules/react-dom/cjs/react-dom.development.js:19148:24)
at renderRoot (node_modules/react-dom/cjs/react-dom.development.js:19231:7)
at performWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:20138:7)
at performWork (node_modules/react-dom/cjs/react-dom.development.js:20050:7)
at performSyncWork (node_modules/react-dom/cjs/react-dom.development.js:20024:3)
at requestWork (node_modules/react-dom/cjs/react-dom.development.js:19893:5)
at scheduleWork (node_modules/react-dom/cjs/react-dom.development.js:19707:5)
at scheduleRootUpdate (node_modules/react-dom/cjs/react-dom.development.js:20368:3)
at updateContainerAtExpirationTime (node_modules/react-dom/cjs/react-dom.development.js:20396:10)
at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:20453:10)
at ReactRoot.Object.<anonymous>.ReactRoot.render (node_modules/react-dom/cjs/react-dom.development.js:20749:3)
at node_modules/react-dom/cjs/react-dom.development.js:20886:14
at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:20255:10)
at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:20882:5)
at Object.render (node_modules/react-dom/cjs/react-dom.development.js:20951:12)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:382:114)
at new ReactWrapper (node_modules/enzyme/build/ReactWrapper.js:134:16)
at mount (node_modules/enzyme/build/mount.js:21:10)
at test (src/test/bmi_calculator.step.test.js:22:21)
at defineScenarioFunction (node_modules/jest-cucumber/src/feature-definition-creation.ts:155:9)
at test (src/test/bmi_calculator.step.test.js:20:3)
at Suite.<anonymous> (node_modules/jest-cucumber/src/feature-definition-creation.ts:279:9)
at defineFeature (node_modules/jest-cucumber/src/feature-definition-creation.ts:278:5)
at Object.<anonymous> (src/test/bmi_calculator.step.test.js:19:1)
我尝试了涉及 Enzyme 的 setContext
的各种解决方案。但不确定这是否与上下文或react-router
或两者有关。
【问题讨论】:
你试过mount(<BrowserRouter><Login /></BrowserRouter)
,其中BrowserRouter
是从react-router
导入的吗?
【参考方案1】:
由于您正在针对context
进行测试,因此理想情况下,您需要在根级别进行测试,并从那里针对任何 DOM 更改进行断言。另请注意,您不能在路由器之外使用Route
(BrowserRouter
、Router
、StaticRouter
、...等),也不能在未连接到路由器的history
之外使用。虽然我从来没有使用过use-react-router
,但仔细看一下,它仍然需要一个路由器。因此,您的测试必须包括Provider
、路由器和您的页面/组件。
这是一个在根级别进行测试的工作示例:
src/root/index.js
import React from "react";
import Provider from "../hooks/useAuthentication";
import Routes from "../routes";
const Root = () => (
<Provider>
<Routes />
</Provider>
);
export default Root;
src/routes/index.js
import React from "react";
import BrowserRouter, Route, Switch from "react-router-dom";
import Container, Header, ProtectedRoutes from "../components";
import About, Dashboard, Home from "../pages";
const Routes = () => (
<BrowserRouter>
<Container>
<Header />
<Switch>
<Route exact path="/" component=Home />
<Route exact path="/about" component=About />
<ProtectedRoutes>
<Route exact path="/dashboard" component=Dashboard />
</ProtectedRoutes>
</Switch>
</Container>
</BrowserRouter>
);
export default Routes;
src/root/__tests__/root.test.js
import React from "react";
import mount from "enzyme";
import Root from "../index";
describe("Authentication", () =>
let wrapper;
beforeAll(() =>
wrapper = mount(<Root />);
wrapper
.find("Router")
.prop("history")
.push("/dashboard");
wrapper.update();
);
afterAll(() =>
wrapper.unmount();
);
it("initially renders a Login component and displays a message", () =>
expect(wrapper.find("h1").text()).toEqual("Login");
expect(wrapper.find("h3").text()).toEqual(
"You must login before viewing the dashboard!"
);
);
it("authenticates the user and renders the Dashboard", () =>
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Dashboard");
);
it("unauthenticates the user and redirects the user to the home page", () =>
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Home");
);
);
Dashboard 页面可以被隔离,只要它可以访问认证功能;但是,这可能会为后续页面/组件创建一些重复的测试用例,并且没有多大意义,因为它仍然需要在根级别和路由器设置上下文(特别是如果组件/页面或钩子订阅history
)。
这是一个已隔离仪表板页面以进行测试的工作示例:
src/routes/index.js
import React from "react";
import BrowserRouter, Route, Switch from "react-router-dom";
import Container, Header, ProtectedRoutes from "../components";
import About, Dashboard, Home from "../pages";
const Routes = () => (
<BrowserRouter>
<Container>
<Header />
<Switch>
<Route exact path="/" component=Home />
<Route exact path="/about" component=About />
<ProtectedRoutes>
<Route exact path="/dashboard" component=Dashboard />
</ProtectedRoutes>
</Switch>
</Container>
</BrowserRouter>
);
export default Routes;
components/ProtectedRoutes/index.js
import React from "react";
import useAuthentication from "../../hooks";
import Login from "../Login";
const ProtectedRoutes = ( children ) =>
const isAuthenticated, login = useAuthentication();
return isAuthenticated ? children : <Login login=login />;
;
export default ProtectedRoutes;
pages/Dashboard/index.js
import React, Fragment, useCallback from "react";
import useAuthentication from "../../hooks";
import Button, Description, Title from "../../components";
const Dashboard = ( history ) =>
const logout = useAuthentication();
const unAuthUser = useCallback(() =>
logout();
history.push("/");
, [history, logout]);
return (
<Fragment>
<Title>Dashboard</Title>
<Description>
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper
suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem
vel eum iriure dolor in hendrerit in vulputate velit esse molestie
consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et
accumsan et iusto odio dignissim qui blandit praesent luptatum zzril
delenit augue duis dolore te feugait nulla facilisi.
</Description>
<Button onClick=unAuthUser>Logout</Button>
</Fragment>
);
;
export default Dashboard;
pages/Dashboard/__tests__/Dashboard.test.js
import React from "react";
import mount from "enzyme";
import BrowserRouter, Route from "react-router-dom";
import Provider from "../../../hooks/useAuthentication";
import ProtectedRoutes from "../../../components";
import Dashboard from "../index";
describe("Dashboard Page", () =>
let wrapper;
beforeAll(() =>
wrapper = mount(
<Provider>
<BrowserRouter>
<ProtectedRoutes>
<Route exact path="/" component=Dashboard />
</ProtectedRoutes>
</BrowserRouter>
</Provider>
);
);
afterAll(() =>
wrapper.unmount();
);
it("initially renders a login component and displays a message", () =>
expect(wrapper.find("h1").text()).toEqual("Login");
expect(wrapper.find("h3").text()).toEqual(
"You must login before viewing the dashboard!"
);
);
it("authenticates the user and updates the component", () =>
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Dashboard");
);
it("unauthenticates the user", () =>
wrapper.find("button").simulate("click");
expect(wrapper.find("h1").text()).toEqual("Login");
);
);
【讨论】:
以上是关于React Context 和 Hooks API 的酶错误的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 React Hooks 和 Context API 正确地将来自 useEffect 内部调用的多个端点的数据添加到状态对象?
React Context with Hooks 总是“落后一步”
mobx中的inject,observer迁移至react Hooks写法