使用 react-testing-library 测试 useContext()

iqb*_*125 9 testing reactjs jestjs react-hooks

我想我找到了另一种使用useContext钩子测试组件的方法。我看过一些教程,这些教程测试一个值是否可以从父上下文提供者成功传递给子组件,但没有找到关于更新上下文值的子组件的教程。

我的解决方案是将根父组件与提供程序一起呈现,因为状态最终在根父组件中更改,然后传递给提供程序,然后提供程序将其传递给所有子组件。对?

测试似乎在应该通过的时候通过,在不应该通过的时候不通过。有人可以解释为什么这是或不是测试useContext钩子的好方法吗?

根父组件:

...
const App = () => {
  const [state, setState] = useState("Some Text")

  const changeText = () => {
    setState("Some Other Text")
  }
...

  <h1> Basic Hook useContext</h1>
     <Context.Provider value={{changeTextProp: changeText,
                               stateProp: state
                                 }} >
        <TestHookContext />
     </Context.Provider>
)}
Run Code Online (Sandbox Code Playgroud)

上下文对象:

import React from 'react';

const Context = React.createContext()

export default Context
Run Code Online (Sandbox Code Playgroud)

子组件:

import React, { useContext } from 'react';

import Context from '../store/context';

const TestHookContext = () => {
  const context = useContext(Context)

  return (
    <div>
    <button onClick={context.changeTextProp}>
        Change Text
    </button>
      <p>{context.stateProp}</p>
    </div>
  )
}
Run Code Online (Sandbox Code Playgroud)

和测试:

import React from 'react';
import ReactDOM from 'react-dom';
import TestHookContext from '../test_hook_context.js';
import {render, fireEvent, cleanup} from '@testing-library/react';
import App from '../../../App'

import Context from '../../store/context';

afterEach(cleanup)

it('Context is updated by child component', () => {

   const { container, getByText } = render(<App>
                                            <Context.Provider>
                                             <TestHookContext />
                                            </Context.Provider>
                                           </App>);

   console.log(container)
   expect(getByText(/Some/i).textContent).toBe("Some Text")

   fireEvent.click(getByText("Change Text"))

   expect(getByText(/Some/i).textContent).toBe("Some Other Text")
})
Run Code Online (Sandbox Code Playgroud)

Jon*_*ski 9

您提到的方法的问题是耦合。您要测试的上下文取决于<TestHookContext/><App/>

React-testing-library 的作者 Kent C. Dodds 有一篇关于“React 测试隔离”的完整文章,如果您想阅读的话。

TLDR:此处为演示存储库

如何测试上下文

  • 导出<ContextProvider>保存状态并返回的组件<MyContext.Provider value={{yourWhole: "State"}}>{children}<MyContext.Provider/>这是我们要为提供者测试的组件。
  • 在使用该 Context 的组件上,创建一个 MockContextProvider 来替换原始的。您想要单独测试该组件。
  • 您可以通过测试根组件来测试整个应用程序工作流程。

测试身份验证提供者

假设我们有一个组件,通过以下方式使用上下文提供身份验证:

import React, { createContext, useState } from "react";

export const AuthContext = createContext();

const AuthProvider = ({ children }) => {
  const [isLoggedin, setIsLoggedin] = useState(false);
  const [user, setUser] = useState(null);

  const login = (user) => {
    setIsLoggedin(true);
    setUser(user);
  };

  const logout = () => {
    setIsLoggedin(false);
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ logout, login, isLoggedin, user }}>
      {children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;
Run Code Online (Sandbox Code Playgroud)

测试文件如下所示:

import { fireEvent, render, screen } from "@testing-library/react";
import AuthProvider, { AuthContext } from "./AuthProvider";
import { useContext } from "react";

const CustomTest = () => {
  const { logout, login, isLoggedin, user } = useContext(AuthContext);

  return (
    <div>
      <div data-testid="isLoggedin">{JSON.stringify(isLoggedin)}</div>
      <div data-testid="user">{JSON.stringify(user)}</div>
      <button onClick={() => login("demo")} aria-label="login">
        Login
      </button>
      <button onClick={logout} aria-label="logout">
        LogOut
      </button>
    </div>
  );
};

test("Should render initial values", () => {
  render(
    <AuthProvider>
      <CustomTest />
    </AuthProvider>
  );

  expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
  expect(screen.getByTestId("user")).toHaveTextContent("null");
});

test("Should Login", () => {
  render(
    <AuthProvider>
      <CustomTest />
    </AuthProvider>
  );
  const loginButton = screen.getByRole("button", { name: "login" });
  fireEvent.click(loginButton);
  expect(screen.getByTestId("isLoggedin")).toHaveTextContent("true");
  expect(screen.getByTestId("user")).toHaveTextContent("demo");
});

test("Should Logout", () => {
  render(
    <AuthProvider>
      <CustomTest />
    </AuthProvider>
  );
  const loginButton = screen.getByRole("button", { name: "logout" });
  fireEvent.click(loginButton);
  expect(screen.getByTestId("isLoggedin")).toHaveTextContent("false");
  expect(screen.getByTestId("user")).toHaveTextContent("null");
});

Run Code Online (Sandbox Code Playgroud)

测试消耗 Context 的组件

import React, { useContext } from "react";
import { AuthContext } from "../context/AuthProvider";

const Welcome = () => {
  const { logout, login, isLoggedin, user } = useContext(AuthContext);

  return (
    <div>
      {user && <div>Hello {user}</div>}
      {!user && <div>Hello Anonymous Goose</div>}
      {!isLoggedin && (
        <button aria-label="login" onClick={() => login("Jony")}>
          Log In
        </button>
      )}
      {isLoggedin && (
        <button aria-label="logout" onClick={() => logout()}>
          Log out
        </button>
      )}
    </div>
  );
};

export default Welcome;
Run Code Online (Sandbox Code Playgroud)

我们将通过提供我们自己的值之一来模拟 AuthContext 值:

import React, { useContext } from "react";
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import Welcome from "./welcome";
import userEvent from "@testing-library/user-event";
import { AuthContext } from "../context/AuthProvider";

// A custom provider, not the AuthProvider, to test it in isolation.
// This customRender will be a fake AuthProvider, one that I can controll to abstract of AuthProvider issues.

const customRender = (ui, { providerProps, ...renderOptions }) => {
  return render(
    <AuthContext.Provider value={providerProps}>{ui}</AuthContext.Provider>,
    renderOptions
  );
};

describe("Testing Context Consumer", () => {
  let providerProps;
  beforeEach(
    () =>
      (providerProps = {
        user: "C3PO",
        login: jest.fn(function (user) {
          providerProps.user = user;
          providerProps.isLoggedin = true;
        }),
        logout: jest.fn(function () {
          providerProps.user = null;
          providerProps.isLoggedin = false;
        }),
        isLoggedin: true,
      })
  );

  test("Should render the user Name when user is signed in", () => {
    customRender(<Welcome />, { providerProps });
    expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello C3PO");
  });

  test("Should render Hello Anonymous Goose when is NOT signed in", () => {
    providerProps.isLoggedin = false;
    providerProps.user = null;
    customRender(<Welcome />, { providerProps });
    expect(screen.getByText(/Hello/i)).toHaveTextContent(
      "Hello Anonymous Goose"
    );
  });

  test("Should render Logout button when user is signed in", () => {
    customRender(<Welcome />, { providerProps });
    expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "login" })).toBeNull();
  });

  test("Should render Login button when user is NOT signed in", () => {
    providerProps.isLoggedin = false;
    providerProps.user = null;
    customRender(<Welcome />, { providerProps });
    expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
  });

  test("Should Logout when user is signed in", () => {
    const { rerender } = customRender(<Welcome />, { providerProps });
    const logout = screen.getByRole("button", { name: "logout" });
    expect(logout).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "login" })).toBeNull();
    userEvent.click(logout);
    expect(providerProps.logout).toHaveBeenCalledTimes(1);

    //Technically, re renders are responsability of the parent component, but since we are here...
    rerender(
      <AuthContext.Provider value={providerProps}>
        <Welcome />
      </AuthContext.Provider>
    );
    expect(screen.getByText(/Hello/i)).toHaveTextContent(
      "Hello Anonymous Goose"
    );
    expect(screen.getByRole("button", { name: "login" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
  });

  test("Should Login when user is NOT signed in", () => {
    providerProps.isLoggedin = false;
    providerProps.user = null;
    const { rerender } = customRender(<Welcome />, { providerProps });
    const login = screen.getByRole("button", { name: "login" });
    expect(login).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "logout" })).toBeNull();
    userEvent.click(login);
    expect(providerProps.login).toHaveBeenCalledTimes(1);

    //Technically, re renders are responsability of the parent component, but since we are here...
    rerender(
      <AuthContext.Provider value={providerProps}>
        <Welcome />
      </AuthContext.Provider>
    );
    expect(screen.getByText(/Hello/i)).toHaveTextContent("Hello Jony");
    expect(screen.getByRole("button", { name: "logout" })).toBeInTheDocument();
    expect(screen.queryByRole("button", { name: "login" })).toBeNull();
  });
});
Run Code Online (Sandbox Code Playgroud)


Joh*_*der 0

您的示例/代码完全正确。(不确定您是否需要安装包装<App />- 您应该直接包装在上下文提供程序中)。

对于你的问题:

测试似乎在应该通过的时候通过,在不应该通过的时候却没有通过。有人可以解释为什么这是或不是测试 useContext() 挂钩的好方法。

使用时测试的好方法useContext(),因为看起来您已经抽象了上下文,以便您的子(消费)组件及其测试都使用相同的上下文。我不明白为什么当您使用相同的上下文提供程序时(正如您在示例中所做的那样),您会模拟或模拟上下文提供程序正在执行的操作。

React 测试库文档指出

您的测试越接近软件的使用方式,它们就越能给您带来信心。

因此,以设置组件的方式设置测试可以实现该目标。如果您在一个应用程序中有多个测试需要包装在同一上下文中,那么这篇博客文章提供了一个用于重用该逻辑的巧妙解决方案。