如何测试反应连接的组件以及组件的测试内容?

How to test a react connected component and what to test of component?

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

我正在尝试通过 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,所以我也删除了它们,但我的测试仍然失败。

我想要实现的是将模拟函数作为 prop 传递并记录它作为 my
执行过一次 onFinish 函数在我的登录组件中被调用,但由于我已经将我的模拟玩笑函数作为道具传递,它没有执行并通过我的测试。

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 | });

问题

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

解决方案

IMO 简单的解决方案,因为您仍在使用 connect 装饰器,是导出 undecorated/unconnected 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 模拟您的商店并断言 was/were 已派发正确的操作。这里的模拟商店只是存储一组要测试的已分派操作。

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!
});

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

  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 存储,并且可以按照您需要的方式初始化状态。

测试行为,而不是实现细节

下一期不考mock/implementation。您的表单是否提交到后端?它是否显示成功消息?您可以使用它来确定测试断言。下面我使用包装的 render() 方法进行了测试,并查找表示成功登录的闪现消息。

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
});

作为用户,登录成功意味着什么?