使用 React Hook 客户端测试 Apollo 查询
Posted
技术标签:
【中文标题】使用 React Hook 客户端测试 Apollo 查询【英文标题】:Testing Apollo Query with React Hook Client 【发布时间】:2020-04-11 15:01:10 【问题描述】:我正在尝试使用 jest 为这个组件编写测试
import useState, useRef from 'react';
import PropTypes from 'prop-types';
import connect from 'react-redux';
import Query from 'react-apollo';
import updateYourDetails from 'universal/domain/health/yourDetails/yourDetailsActions';
import Input from 'universal/components/input/input';
import InputNumber from 'universal/components/input/inputNumber/inputNumber';
import AsyncButton from 'universal/components/asyncButton/asyncButton';
import ErrorMessage from 'universal/components/errorMessage/errorMessage';
import Link from 'universal/components/link/link';
import analytics from 'universal/utils/analytics/analytics';
import isChatAvailable from 'universal/logic/chatLogic';
import validators from 'universal/utils/validation';
import localTimezone, getWeekdays from 'universal/utils/date';
import
CALL_ME_BACK_LOADING_MSG,
CALL_ME_BACK_LABELS_SCHEDULE_TIME,
CALL_ME_BACK_LABELS_SELECTED_DATE,
CALL_ME_BACK_ERROR_MSG,
CALL_ME_BACK_TEST_PARENT_WEEKDAY,
CALL_ME_BACK_TEST_CHILD_WEEKDAY,
from 'universal/constants/callMeBack';
import CallCenterAvailibility from './CallCenterAvailibility';
import SelectWrapper from './SelectWrapper';
import SelectOption from './SelectOption';
import styles from './callMeBackLightBox.css';
import CALL_ME_BACK_QUERY from './callMeBackQuery';
import postData from './postData';
export const CallMeForm = props =>
const initSelectedDate = getWeekdays()
.splice(0, 1)
.reduce(acc => ( ...acc ));
const onSubmissionComplete, className, variant = props;
const [hasSuccessfullySubmitted, setHasSuccessfullySubmitted] = useState(false);
const [apiStatus, setApiStatus] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [cellNumber, setCallNumber] = useState(props.cellNumber || '');
const [customerFirstName, setCustomerFirstName] = useState(props.customerFirstName || '');
const [number, setNumber] = useState(props.Number || '');
const [selectedDate, setSelectedDate] = useState(initSelectedDate || '');
const [scheduledTime, setScheduledTime] = useState('');
const weekdays = getWeekdays() || [];
const timezone = localTimezone || '';
const requestReceived = apiStatus === 'CALLBACK_ALREADY_EXIST';
const cellNumberInput = useRef(null);
const customerFirstNameInput = useRef(null);
const getQuery = () => (
<Query query=CALL_ME_BACK_QUERY variables= weekday: selectedDate.weekday >
( data, error, loading ) =>
if (loading)
return (
<SelectWrapper disabled labelTitle=CALL_ME_BACK_LABELS_SCHEDULE_TIME name="scheduledTime">
<SelectOption label=CALL_ME_BACK_LOADING_MSG />
</SelectWrapper>
);
if (error) return <ErrorMessage hasError errorMessage=<p>CALL_ME_BACK_ERROR_MSG</p> />;
return (
<CallCenterAvailibility
selectedDate=selectedDate
callCenterBusinessHour=data.callCenterBusinessHour
onChange=val => setScheduledTime(val)
/>
);
</Query>
);
const getPostSubmitMessage = (firstName: string, type: string) =>
const messages =
callCentreClosed: `a`,
requestReceived: `b`,
default: `c`,
;
return `Thanks $firstName, $messages[type] || messages.default`;
;
const validate = () =>
const inputs = [customerFirstNameInput, cellNumberInput];
const firstInvalidIndex = inputs.map(input => input.current.validate()).indexOf(false);
const isValid = firstInvalidIndex === -1;
return isValid;
;
const onSubmitForm = event =>
event.preventDefault();
onSubmit();
;
const onSubmit = async () =>
if (variant === '0' && !validate())
return;
analytics.track(analytics.events.callMeBack.callMeBackSubmit,
trackingSource: 'Call Me Form',
);
setIsLoading(true);
const srDescription = '';
const response = await postData(
cellNumber,
customerFirstName,
number,
scheduledTime,
timezone,
srDescription,
);
const status = response;
const updatedSubmissionFlag = status === 'CALLBACK_ALREADY_EXIST' || status === 'CALLBACK_ADDED_SUCCESSFULLY';
// NOTE: add a slight delay for better UX
setTimeout(() =>
setApiStatus(apiStatus);
setIsLoading(false);
setHasSuccessfullySubmitted(updatedSubmissionFlag);
, 400);
// Update Redux store
updateYourDetails(
mobile: cellNumber,
firstName: customerFirstName,
);
if (onSubmissionComplete)
onSubmissionComplete();
;
if (hasSuccessfullySubmitted)
return (
<p aria-live="polite" role="status">
getPostSubmitMessage(
customerFirstName,
(!requestReceived && !isChatAvailable() && 'callCentreClosed') || (requestReceived && 'requestReceived')
)
</p>
);
return (
<form onSubmit=onSubmitForm className=className>
variant !== '1' && (
<>
<label htmlFor="customerFirstName" className=styles.inputLabel>
First name
</label>
<Input
className=styles.input
initialValue=customerFirstName
isMandatory
maxLength=20
name="customerFirstName"
onChange=val => setCustomerFirstName(val)
ref=customerFirstNameInput
value=customerFirstName
...validators.plainCharacters
/>
</>
)
variant !== '1' && (
<>
<label htmlFor="cellNumber" className=styles.inputLabel>
Mobile number
</label>
<Input
className=styles.input
initialValue=cellNumber
isMandatory
maxLength=10
name="cellNumber"
onChange=val => setCallNumber(val)
ref=cellNumberInput
type="tel"
value=cellNumber
...validators.tel
/>
</>
)
variant !== '1' && (
<>
' '
<label htmlFor="number" className=styles.inputLabel>
Qantas Frequent Flyer number (optional)
</label>
<InputNumber
className=styles.input
disabled=Boolean(props.number)
initialValue=number
name="number"
onChange=val => setNumber(val)
value=number
/>
</>
)
weekdays && (
<>
<SelectWrapper
testId=`$CALL_ME_BACK_TEST_PARENT_WEEKDAY`
labelTitle=CALL_ME_BACK_LABELS_SELECTED_DATE
name="selectedDate"
onChange=val =>
setSelectedDate(
...weekdays.filter(( value ) => value === val).reduce(acc => ( ...acc )),
)
tabIndex=0
>
weekdays.map(( value, label , i) => (
<SelectOption
testId=`$CALL_ME_BACK_TEST_CHILD_WEEKDAY-$i`
key=value
label=label
value=value
/>
))
</SelectWrapper>
getQuery()
</>
)
<AsyncButton className=styles.submitButton onClick=onSubmit isLoading=isLoading>
Call me
</AsyncButton>
<ErrorMessage
hasError=(apiStatus >= 400 && apiStatus < 600) || apiStatus === 'Failed to fetch'
errorMessage=
<p>
There was an error submitting your request to call you back. Please try again or call us at' '
<Link href="tel:134960">13 49 60</Link>.
</p>
/>
</form>
);
;
CallMeForm.propTypes =
cellNumber: PropTypes.string,
customerFirstName: PropTypes.string,
number: PropTypes.string,
onSubmissionComplete: PropTypes.func,
className: PropTypes.string,
variant: PropTypes.string,
;
const mapStateToProps = state =>
const frequentFlyer, yourDetails = state;
return
cellNumber: yourDetails.mobile,
customerFirstName: yourDetails.firstName,
number: frequentFlyer.memberNumber,
;
;
export default connect(mapStateToProps)(CallMeForm);
我的测试文件如下
import render, cleanup from '@testing-library/react';
import MockedProvider from 'react-apollo/test-utils';
import shallow from 'enzyme';
import MockDate from 'mockdate';
import isChatAvailable from 'universal/logic/chatLogic';
import CALL_ME_BACK_QUERY from './callMeBackQuery';
import CallMeForm from './CallMeForm';
import postData from './postData';
jest.mock('universal/components/input/input', () => 'Input');
jest.mock('universal/components/asyncButton/asyncButton', () => 'AsyncButton');
jest.mock('universal/components/errorMessage/errorMessage', () => 'ErrorMessage');
jest.mock('universal/logic/chatLogic');
jest.mock('./postData');
describe('CallMeForm', () =>
let output;
beforeEach(() =>
jest.resetModules();
jest.resetAllMocks();
const mockQueryData = [
client:,
request:
query: CALL_ME_BACK_QUERY,
variables: weekday: '' ,
,
result:
data:
callCenterBusinessHour:
timeStartHour: 9,
timeStartMinute: 0,
timeEndHour: 5,
timeEndMinute: 0,
closed: false,
,
,
,
,
];
const container = render(<MockedProvider mocks=mockQueryData addTypename=false><CallMeForm /></MockedProvider>);
output = container;
);
afterEach(cleanup);
it('renders correctly', () =>
expect(output).toMatchSnapshot();
);
);
我不断收到错误:TypeError: this.state.client.stop is not a function
我还删除了 <MockedProvider>
包装器,我得到另一个错误 Invariant Violation: could not find "client" in the context or pass in as a prop。将根组件包装在一个 中,或者将一个 ApolloClient 实例传递给
通过道具。
有谁知道我为什么会收到这个错误以及如何解决这个问题?
【问题讨论】:
【参考方案1】:我没有解决办法,但我有一些信息。
首先,我在这里遇到了同样的错误,使用@testing-library/react
渲染。
然后我尝试使用 ReactDOM 进行渲染,如下所示:
// inside the it() call with async function
const container = document.createElement("div");
ReactDOM.render(
< MockedProvider ...props>
<MyComponent />
</MockedProvider>,
container
);
await wait(0);
expect(container).toMatchSnapshot();
并且还尝试使用 Enzyme 进行渲染,像这样:
// inside the it() call, with async function too
const wrapper = mount(
<MockedProvider ...props>
<MyComponent />
</MemoryRouter>
);
await wait(0);
expect(wrapper.html()).toMatchSnapshot();
ReactDOM 和 Enzyme 方法都运行良好。
关于我们得到的错误,我想可能与@testing-library/react
有关=/
我没有尝试使用react-test-renderer
进行渲染,也许它也可以。
嗯,这就是我得到的……也许它对你有所帮助。
Ps.:关于waait
:https://www.apollographql.com/docs/react/development-testing/testing/#testing-final-state
2020 年 2 月 5 日编辑:
基于https://github.com/apollographql/react-apollo/pull/2165#issuecomment-478865830,我找到了该解决方案(它看起来很丑但有效¯\_(ツ)_/¯):
<MockedProvider ...props>
<ApolloConsumer>
client =>
client.stop = jest.fn();
return <MyComponent />;
</ApolloConsumer>
</MockedProvider>
【讨论】:
【参考方案2】:我遇到了同样的问题并且能够解决它。我缺少对等依赖。
您的 package.json 未显示,所以我不确定您的问题是否与我的相同,但我能够通过安装“apollo-client”解决问题。
我正在为我的客户端使用 AWS Appsync,因此没有安装 apollo-client。
【讨论】:
以上是关于使用 React Hook 客户端测试 Apollo 查询的主要内容,如果未能解决你的问题,请参考以下文章
如何测试使用自定义 TypeScript React Hook 的组件?
如何测试使用自定义TypeScript React Hook的组件?
我应该如何测试使用 Typescript 进行 api 调用的 React Hook “useEffect”?