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 的 shallowmount 出现奇怪的错误外,一切都按预期工作。

我正在尝试这样测试:

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(&lt;BrowserRouter&gt;&lt;Login /&gt;&lt;/BrowserRouter),其中BrowserRouter是从react-router导入的吗? 【参考方案1】:

由于您正在针对context 进行测试,因此理想情况下,您需要在根级别进行测试,并从那里针对任何 DOM 更改进行断言。另请注意,您不能在路由器之外使用RouteBrowserRouterRouterStaticRouter、...等),也不能在未连接到路由器的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 总是“落后一步”

React API

mobx中的inject,observer迁移至react Hooks写法

React 系列 02❤️ Custom Hooks 中使用 React Context

如何将 React Context 与 Hooks 一起使用?