React Context 和 Hooks 的酶错误 API

Enzyme errors with React Context and Hooks API

我创建了这个 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

堆栈跟踪的第二个错误:

   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 的解决方案。但不确定这是否与 Context 或 react-router 或两者有关。

由于您要针对 context 进行测试,理想情况下,您将希望在根级别进行测试并针对从那里进行的任何 DOM 更改做出断言。另请注意,您不能在路由器(BrowserRouterRouterStaticRouter、...等)之外使用 Route,也不能在没有路由器的情况下使用 history '已连接到路由器。虽然我从来没有用过use-react-router,但仔细一看,它仍然需要一个路由器。因此,您的测试必须包括 Provider、路由器和您的 page/component。

这是在根级别进行测试的工作示例

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页面只要能访问认证功能就可以隔离;然而,这可能会为后续的 pages/components 创建一些重复的测试用例并且没有多大意义,因为它仍然需要在根级别设置上下文和路由器(特别是如果 component/page 或hook 正在订阅 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");
  });
});