代码之家  ›  专栏  ›  技术社区  ›  theJuls

使用jest和Ezyme在基于类的组件上通过上下文设置mockFunction

  •  0
  • theJuls  · 技术社区  · 5 年前

    我有一个非常简单的基于类的组件。如下所示:

    class MyComponent extends React.Component {
        onPressButton () {
            console.warn('button pressed')
            const { contextFunction } = this.context
            contextFunction()
        }
    
        render () {
            return (
                <div>
                    My Component
                    <button onClick={() => onPressButton()}>Press button</button>
                </div>
            )
        }
    }
    
    
    MyComponent.contextType = SomeContext
    

    这一切都很好,正如预期的那样。然而,我在添加带有jest和Ezyme的单元测试时遇到了困难。我当前的代码如下所示:

    test('should render test Press button', async () => {
        const contextFunctionMock = jest.fn()
        const wrapper = shallow(<MyComponent {...props} />)
        wrapper.instance().context = { contextFunction: contextFunctionMock }
    
        console.log('wrapper.instance()', wrapper.instance())
    
        await wrapper.instance().onPressButton() // this works just fine
        expect(contextFunctionMock).toHaveBeenCalled() // this errors, basically because ti complains contextFunction is not a function
    })
    

    正如你在上面看到的,我安慰你。记录我的 wrapper.instance() 看看发生了什么。 有趣的是 context 在实例对象的根上,实际上是我所期望的基于设置上下文的内容,这类似于:

    context: {
            contextFunction: [Function: mockConstructor] {
              _isMockFunction: true,
              getMockImplementation: [Function (anonymous)],
              [...Other mock function properties]
            }
    ...
    

    然而,还有第二种情况,即 updater 包装器的属性。实例(),它是一个空对象。基本上如下所示:

    updater: <ref *2> Updater {
            _renderer: ReactShallowRenderer {
              _context: {},
              ...
            }
    

    不确定这是否是用于组件单元测试的上下文,但它目前只是一个空对象,这让我认为这可能就是用于它的上下文。

    无论如何,我怎样才能正确地模拟我的上下文函数来在这个特定的单元测试上运行呢?还有,为什么这种情况会发生,但在其他类似情况下不会发生?

    0 回复  |  直到 5 年前
        1
  •  1
  •   Matt Carlotta    5 年前

    问题

    上面代码的一个基本问题是,无法断言上下文函数是 成功地 / 弱点 被称为。现在,你点击了一个按钮,但没有任何迹象表明发生了什么 之后 单击按钮(上下文/组件中没有任何内容被更改/更新,以反映任何类型的UI更改)。因此,如果没有上下文函数,那么断言调用了上下文函数是没有好处的 后果 点击按钮。

    除上述内容外,酶适配器不支持使用 createContext 方法

    然而,有一个解决这个限制的办法!您不需要对组件进行单元测试,而是需要创建一个与上下文的集成测试。您将对上下文函数进行断言,而不是断言调用了上下文函数 后果 点击改变上下文的按钮,以及它对组件的影响。

    解决方案

    由于组件与上下文中的内容相关联,因此您将创建一个集成测试。例如,您将在测试中使用上下文包装组件,并对结果进行断言:

    import * as React from "react";
    import { mount } from "enzyme";
    import Component from "./path/to/Component";
    import ContextProvider from "./path/to/ContextProvider";
    
    const wrapper = mount(
      <ContextProvider>
        <Component /> 
      </ContextProvider>
    );
    
    it("updates the UI when the button is clicked", () => {
      wrapper.find("button").simulate("click");
    
      expect(wrapper.find(...)).toEqual(...);
    })
    

    通过执行上述操作,您可以在 Component .此外,通过使用 mount ,你不必 dive 进入 ContextProvider 查看 组成部分 加成

    演示示例

    本演示利用上下文将主题从“亮”切换到“暗”,反之亦然。点击 Tests 选项卡来运行 App.test.js 集成测试。

    Edit Theme Context Testing

    代码示例

    应用程序。js

    import * as React from "react";
    import { ThemeContext } from "./ThemeProvider";
    import "./styles.css";
    
    class App extends React.PureComponent {
      render() {
        const { theme, toggleTheme } = this.context;
        return (
          <div className="app">
            <h1>Current Theme</h1>
            <h2 data-testid="theme" className={`${theme}-text`}>
              {theme}
            </h2>
            <button
              className={`${theme}-button button`}
              data-testid="change-theme-button"
              type="button"
              onClick={toggleTheme}
            >
              Change Theme
            </button>
          </div>
        );
      }
    }
    
    App.contextType = ThemeContext;
    
    export default App;
    

    这是我的提供者。js

    import * as React from "react";
    
    export const ThemeContext = React.createContext();
    
    class ThemeProvider extends React.Component {
      state = {
        theme: "light"
      };
    
      toggleTheme = () => {
        this.setState((prevState) => ({
          theme: prevState.theme === "light" ? "dark" : "light"
        }));
      };
    
      render = () => (
        <ThemeContext.Provider
          value={{ theme: this.state.theme, toggleTheme: this.toggleTheme }}
        >
          {this.props.children}
        </ThemeContext.Provider>
      );
    }
    
    export default ThemeProvider;
    

    指数js

    import * as React from "react";
    import ReactDOM from "react-dom";
    import ThemeProvider from "./ThemeProvider";
    import App from "./App";
    
    ReactDOM.render(
      <React.StrictMode>
        <ThemeProvider>
          <App />
        </ThemeProvider>
      </React.StrictMode>,
      document.getElementById("root")
    );
    

    测试示例

    这是一个如何对照上面的演示示例进行测试的示例。

    主题。js (可选的可重用测试工厂函数,用于使用上下文包装组件——在您可能需要调用 wrapper.setProps() 在根目录上更新组件的道具)

    import * as React from "react";
    import { mount } from "enzyme";
    import ThemeProvider from "./ThemeProvider";
    
    /**
     * Factory function to create a mounted wrapper with context for a React component
     *
     * @param Component - Component to be mounted
     * @param options - Optional options for enzyme's mount function.
     * @function createElement - Creates a wrapper around passed in component with incoming props (now we can use wrapper.setProps on root)
     * @returns ReactWrapper - a mounted React component with context.
     */
    export const withTheme = (Component, options = {}) =>
      mount(
        React.createElement((props) => (
          <ThemeProvider>{React.cloneElement(Component, props)}</ThemeProvider>
        )),
        options
      );
    
    export default withTheme;
    

    应用程序。测验js

    import * as React from "react";
    import { configure } from "enzyme";
    import Adapter from "enzyme-adapter-react-16";
    import withTheme from "./withTheme";
    import App from "./App";
    
    configure({ adapter: new Adapter() });
    
    // wrapping "App" with some context
    const wrapper = withTheme(<App />);
    
    /*
      THIS "findByTestId" FUNCTION IS OPTIONAL! 
     
      I'm using "data-testid" attributes, since they're static properties in 
      the DOM that are easier to find within a "wrapper". 
      This is 100% optional, but easier to use when a "className" may be 
      dynamic -- such as when using css modules that create dynamic class names.
    */
    const findByTestId = (id) => wrapper.find(`[data-testid='${id}']`);
    
    describe("App", () => {
      it("initially displays a light theme", () => {
        expect(findByTestId("theme").text()).toEqual("light");
        expect(findByTestId("theme").prop("className")).toEqual("light-text");
    
        expect(findByTestId("change-theme-button").prop("className")).toContain(
          "light-button"
        );
      });
    
      it("clicking on the 'Change Theme' button toggles the theme between 'light' and 'dark'", () => {
        // change theme to "dark"
        findByTestId("change-theme-button").simulate("click");
    
        expect(findByTestId("theme").text()).toEqual("dark");
        expect(findByTestId("theme").prop("className")).toEqual("dark-text");
        expect(findByTestId("change-theme-button").prop("className")).toContain(
          "dark-button"
        );
    
        // change theme to "light"
        findByTestId("change-theme-button").simulate("click");
    
        expect(findByTestId("theme").text()).toEqual("light");
      });
    });
    
        2
  •  1
  •   diedu    5 年前

    至于今天,新的上下文API是 not supported 通过酶,我找到的唯一解决办法就是使用这个工具 https://www.npmjs.com/package/shallow-with-context

    import { configure, shallow } from "enzyme";
    import Adapter from "enzyme-adapter-react-16";
    import { withContext } from "shallow-with-context";
    import MyComponent from "./MyComponent";
    
    configure({ adapter: new Adapter() });
    
    describe("Context", () => {
      it("should render test Press button", async () => {
        const contextFunctionMock = jest.fn();
        const context = { contextFunction: contextFunctionMock };
        const MyComponentWithContext = withContext(MyComponent, context);
        const wrapper = shallow(<MyComponentWithContext />, { context });
        await wrapper.instance().onPressButton();
        expect(contextFunctionMock).toHaveBeenCalled();
      });
    });
    

    https://codesandbox.io/s/enzyme-context-test-xhfj3?file=/src/MyComponent.test.tsx