如何测试反应连接的组件以及组件的测试内容?
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
高阶组件注入它。 (您删除了 mapStateToProps
和 mapDispatchToProps
但您仍然在 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!
});
我认为我们应该解决的整体测试方法有几个关键问题:
- 我们如何测试 Redux 连接的组件?
- 我们如何测试行为?
通过劫持测试连接组件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
});
作为用户,登录成功意味着什么?
以下是我的学习应用中的一个简单登录组件。
我正在尝试通过 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
高阶组件注入它。 (您删除了 mapStateToProps
和 mapDispatchToProps
但您仍然在 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!
});
我认为我们应该解决的整体测试方法有几个关键问题:
- 我们如何测试 Redux 连接的组件?
- 我们如何测试行为?
通过劫持测试连接组件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
});
作为用户,登录成功意味着什么?