【问题标题】:How to test a react connected component and what to test of component?如何测试一个反应连接的组件以及测试什么组件?
【发布时间】:2021-05-13 21:22:39
【问题描述】:

以下是我的学习应用程序中的一个简单登录组件。

我正在尝试通过 jest 和 testing-library/react 编写以下组件的测试

import React, { useEffect } from 'react';
import { Form, Input, Button, Checkbox } from 'antd';
import { connect } from "react-redux";
import { userLoginThunk } from "../Thunk/LoginThunk";
import { getUserState } from '../selector';

const Login = ({ onFormSubmitFailed, onLoginPressed, history }) => {
    const layout = {
        labelCol: { span: 8 },
        wrapperCol: { span: 8 },
    };
    const tailLayout = {
        wrapperCol: { offset: 8, span: 16 },
    };
    const onFinish = (values) => {
        onLoginPressed(values, history);
    };

    const onFinishFailed = (errorInfo) => {
        onFormSubmitFailed();
    };
    
    return (
        <Form data-testid="loginForm"
            {...layout}
            style={{
                padding: '30vh',
                alignItems: 'center'
            }}
            name="basic"
            initialValues={{ remember: true }}
            onFinish={onFinish}
            onFinishFailed={onFinishFailed}
        >
            <Form.Item
                label="Username"
                name="username"
                rules={[{ required: true, message: 'Please input your username!' }]}
            >
                <Input data-testid="UsernameInput" placeholder="UserName" />
            </Form.Item>

            <Form.Item
                label="Password"
                name="password"
                rules={[{ required: true, message: 'Please input your password!' }]}
            >
                <Input.Password data-testid="PasswordInput" placeholder="Password" />
            </Form.Item>

            <Form.Item {...tailLayout}>
                <Button data-testid="submitButton" type="primary" htmlType="submit">
                    Submit
                 </Button>
            </Form.Item>
        </Form>
    );
};


// const mapStateToProps = state => ({
//     logingIn: getUserState(state),
// });

// const mapDispatchToProp = dispatch => ({
//     onLoginPressed: (userObj, history) => dispatch(userLoginThunk(userObj, history)),
// });
export default connect((state) => ({
    logingIn: getUserState(state)
}), {
    onLoginPressed: (userObj, history) => userLoginThunk(userObj, history)
})(Login);
//export default connect(mapStateToProps, mapDispatchToProp)(Login);

我在讨论中读到我们不需要 mapStateToProps、mapDispatchToProp,所以我也删除了它们,但我的测试仍然失败。

我想要实现的是传递一个模拟函数作为道具并记录它被执行一次,因为我的
onFinish 函数在我的登录组件中被调用,但是当我已经通过了我的模拟 jest 函数作为它没有执行并通过我的测试的道具。

test.only("Login On Submit", () => {
        const onLoginPressed = jest.fn();   
        //(() => {throw new Error('QWERTY')});
        // // (values, history) => {
        // //     console.log('Test');
        // //     console.log('inside Test', values)
        // // }
        const { getByTestId } = render(
            <Provider store={storeMock}>
                <Login onLoginPressed={onLoginPressed} />
            </Provider>
        );
        fireEvent.change(screen.getByTestId(/UsernameInput/i), {
            target: { value: 'admin' }
        });
        fireEvent.change(screen.getByTestId(/PasswordInput/i), {
            target: { value: 'admin' }
        });
        fireEvent.click(screen.getByTestId("submitButton"));
        expect(onLoginPressed).toHaveBeenCalled();

    });

测试结果 期望(jest.fn()).toHaveBeenCalled()

Expected number of calls: >= 1
Received number of calls:    0

  90 |         fireEvent.click(screen.getByTestId("submitButton"));
  91 |         console.log("debug after testUtil render", debug());
> 92 |         expect(onLoginPressed).toHaveBeenCalled();
     |                                ^
  93 | 
  94 |     });
  95 | });

【问题讨论】:

    标签: javascript reactjs react-redux jestjs react-testing-library


    【解决方案1】:

    我认为我们应该解决整体测试方法的几个关键问题:

    1. 我们如何测试 Redux 连接的组件?
    2. 我们如何测试行为?

    通过劫持render()测试连接组件

    我们的团队有一个 testUtils 包装器,它劫持 react-testing-library render() 方法并使用 Redux Provider 包装组件。您可以在Redux docs 上找到它。

    // test-utils.js
    import React from 'react'
    import { render as rtlRender } from '@testing-library/react'
    import { createStore } from 'redux'
    import { Provider } from 'react-redux'
    // Import your own reducer
    import reducer from '../reducer'
    
    function render(
      ui,
      {
        initialState,
        store = createStore(reducer, initialState),
        ...renderOptions
      } = {}
    ) {
      function Wrapper({ children }) {
        return <Provider store={store}>{children}</Provider>
      }
      return rtlRender(ui, { wrapper: Wrapper, ...renderOptions })
    }
    
    // re-export everything
    export * from '@testing-library/react'
    // override render method
    export { render }
    

    这个解决方案最酷的地方在于你得到了你的 Redux 存储并且你可以按照你需要的方式初始化状态。

    测试行为,而非实现细节

    对于下一个问题,我不会测试模拟/实现。您的表单是否提交到后端?它是否显示成功消息?您可以使用它来确定测试断言。下面我将您的测试与包装的 render() 方法一起使用,并查找表示成功登录的 Flash 消息。

    test.only("<Login/>", async () => {
       // You can stub the server response with nock or msw
        nock(`${yoursite}`)
          .post('/someApiEndpoint')
          .reply(200);
        
        const { getByTestId } = render(
          <Login />
        );
    
        fireEvent.change(screen.getByTestId(/UsernameInput/i), {
            target: { value: 'admin' }
        });
        fireEvent.change(screen.getByTestId(/PasswordInput/i), {
            target: { value: 'admin' }
        });
        fireEvent.click(screen.getByTestId("submitButton"));
        
        await waitFor(() => expect(getById('login-success')).toBeTruthy()); // <-- This is missing
    });
    

    作为用户,什么表示成功登录?

    【讨论】:

      【解决方案2】:

      问题

      这里的问题是你的组件仍然有来自 redux 的 props 由 connect 高阶组件注入。 (您删除了 mapStateToPropsmapDispatchToProps,但您仍然在 connect 中内联“定义”了它们)注入的 onLoginPressed 正在覆盖您手动传递的 jest 模拟函数。

      解决方案

      IMO 简单的解决方案,因为您仍在使用 connect 装饰器,所以导出 未装饰/未连接 Login 组件,以便您可以传递通常由 redux 提供的道具.

      export Login = ({ onFormSubmitFailed, onLoginPressed, history }) => {...
      

      测试

      import { Login } from './path/to/Login'; // <-- named import
      
      ...
      
      test.only("Login On Submit", () => {
          const onLoginPressed = jest.fn();
          const { getByTestId } = render(Login onLoginPressed={onLoginPressed} />);
      
          fireEvent.change(screen.getByTestId(/UsernameInput/i), {
              target: { value: 'admin' }
          });
          fireEvent.change(screen.getByTestId(/PasswordInput/i), {
              target: { value: 'admin' }
          });
          fireEvent.click(screen.getByTestId("submitButton"));
      
          expect(onLoginPressed).toHaveBeenCalled();
      
      });
      

      解决方案 #2

      使用 redux-mock-store 模拟您的商店,并断言已发送/已发送正确的操作。在这里,mock store 只是存储了一组分发的动作来进行测试。

      test.only("Login On Submit", () => {
          const store = mockStore({});
          const { getByTestId } = render(
              <Provider store={store}>
                  <Login onLoginPressed={onLoginPressed} />
              </Provider>
          );
      
          fireEvent.change(screen.getByTestId(/UsernameInput/i), {
              target: { value: 'admin' }
          });
          fireEvent.change(screen.getByTestId(/PasswordInput/i), {
              target: { value: 'admin' }
          });
          fireEvent.click(screen.getByTestId("submitButton"));
      
          const actions = store.getActions();
          expect(actions).toEqual([{ type: 'LOGIN' }]); // <-- your action type here!
      });
      

      【讨论】:

      • const loginMockStore = mockStore({ onLoginPressed: { onLogin }, user: { isProcessLoading: false, isRouteAllowed: false, data: [] }, members: { isProcessLoading: false, list: [], detail : 空值 } }); expect(onLogin).toHaveBeenCalled();甚至模拟商店这个测试失败注入的 onLoginPressed 正在覆盖您手动传递的 jest 模拟函数。虽然我现在提供了自定义模拟商店
      • @BilalShaikh 如果使用redux-mock-store,那么在配置模拟存储之后它只消耗一个初始状态对象。模拟存储实际上只跟踪发送给它的操作。由于您使用的是 Thunks,因此您还需要将该中间件添加到您的模拟存储中。如果您不使用redux-mock-store,那么我认为您可能需要包含有关如何创建模拟 redux 商店的详细信息。
      • 我添加了这个,然后我试图期待这个动作,它起作用了 await waitFor(() => { 也许我需要再次浏览文档,因为我不确定我们为什么需要等待获得行动
      • @BilalShaikh 是的,我确定这是因为您正在测试异步 thunk 操作,因此您可能只需要等待它们完成。请参阅自述文件中的asynchronous-actions 部分。
      猜你喜欢
      • 2017-01-15
      • 2019-09-28
      • 1970-01-01
      • 1970-01-01
      • 2016-12-24
      • 1970-01-01
      • 2017-12-04
      • 2018-12-02
      相关资源
      最近更新 更多