测试时在 React 组件回调上使用 sinon fakes
Using sinon fakes on React component callbacks when testing
我遇到了无法解释的事情。我有以下测试:
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import * as sinon from 'sinon';
import TestApp from '../../views/App.test';
import ReplicationForm from './ReplicationForm';
describe('ReplicationForm component', () => {
it('calls saveUrl on submit with new url', () => {
const saveUrlFake = sinon.fake();
const otherFake = (url: string) => console.log(url);
render(<ReplicationForm saveUrl={otherFake} />, { wrapper: TestApp });
(screen.getByLabelText('Hostname') as HTMLInputElement).value = 'https://example.com';
(screen.getByLabelText('Database') as HTMLInputElement).value = 'poi';
(screen.getByLabelText('Username') as HTMLInputElement).value = 'shaw';
(screen.getByLabelText('Password') as HTMLInputElement).value = 'IMissYouR00t';
userEvent.click(screen.getByRole('button', { name: 'Submit' }));
console.log(saveUrlFake.callCount);
expect(saveUrlFake.lastCall.firstArg).to.equal('https://shaw:IMissYouR00t@example.com/poi');
});
});
当我使用 otherFake
时,回调被正确调用,所需的输出被打印到控制台。不幸的是,如果我使用 saveUrlFake
它永远不会被调用。 saveUrlFake.callCount
是 0
并且没有要检查的参数。
奇怪的是,当我用 saveUrlFake('https://shaw:IMissYouR00t@example.com/poi')
调用 saveUrlFake
时,测试成功了。我的问题是,为什么我的 sinon fake 没有被 React 组件调用?
tl;博士
有问题的完整组件:
import styled from '@emotion/styled';
import { Button, TextField, withTheme } from '@material-ui/core';
import React from 'react';
import { useForm } from 'react-hook-form';
import { FormattedMessage, useIntl } from 'react-intl';
const StyledForm = withTheme(
styled.form`
display: flex;
flex-direction: column;
& > *:not(div:first-of-type) {
margin-top: ${({ theme }) => theme.spacing(2)}px;
}
`
);
type FormModel = {
origin: string;
database: string;
username: string;
password: string;
};
type ReplicationFormProps = {
url?: string;
saveUrl: (url: string) => void;
};
export default function ReplicationForm({ url, saveUrl }: ReplicationFormProps) {
const intl = useIntl();
const getDefaultValuesFromURL = (inputUrl: string): FormModel => {
const { origin, username, password, pathname } = new URL(inputUrl);
return {
origin,
username,
password,
database: pathname.replaceAll('/', '')
};
};
const defaultValues: FormModel = url
? getDefaultValuesFromURL(url)
: {
origin: '',
database: '',
username: '',
password: ''
};
const { handleSubmit, formState, register } = useForm<FormModel>({ defaultValues });
const onSubmit = (data: FormModel) => {
const newUrl = new URL(data.origin);
newUrl.username = data.username;
newUrl.password = data.password;
newUrl.pathname = `/${data.database}`;
saveUrl(newUrl.toString());
};
return (
<StyledForm onSubmit={handleSubmit(onSubmit)}>
<TextField
id="replication-form-origin"
name="origin"
inputRef={register}
label={intl.formatMessage({ id: 'label.hostname', defaultMessage: 'Hostname' })}
fullWidth
/>
<TextField
id="replication-form-database"
name="database"
inputRef={register}
label={intl.formatMessage({ id: 'label.database', defaultMessage: 'Database' })}
fullWidth
/>
<TextField
id="replication-form-username"
name="username"
inputRef={register}
label={intl.formatMessage({ id: 'label.username', defaultMessage: 'Username' })}
fullWidth
/>
<TextField
id="replication-form-password"
name="password"
type="password"
inputRef={register}
label={intl.formatMessage({ id: 'label.password', defaultMessage: 'Password' })}
fullWidth
/>
<Button type="submit" color="primary" disabled={formState.isDirty}>
<FormattedMessage id="action.submit" defaultMessage="Submit" />
</Button>
</StyledForm>
);
}
回调好像是异步调用的,需要等待。测试库提供 waitFor()
,在这种情况下会有所帮助。
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import * as sinon from 'sinon';
import TestApp from '../../views/App.test';
import ReplicationForm from './ReplicationForm';
describe('ReplicationForm component', () => {
it('calls saveUrl on submit with new url', async () => {
const saveUrlFake = sinon.fake();
render(<ReplicationForm saveUrl={saveUrlFake} />, { wrapper: TestApp });
userEvent.type(screen.getByLabelText('Hostname *') as HTMLInputElement, 'https://example.com');
userEvent.type(screen.getByLabelText('Database *'), 'poi');
userEvent.type(screen.getByLabelText('Username *'), 'shaw');
userEvent.type(screen.getByLabelText('Password *'), 'IMissYouR00t');
userEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => expect(saveUrlFake.called).to.be.true);
expect(saveUrlFake.lastCall.firstArg).to.equal('https://shaw:IMissYouR00t@example.com/poi');
});
});
我遇到了无法解释的事情。我有以下测试:
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import * as sinon from 'sinon';
import TestApp from '../../views/App.test';
import ReplicationForm from './ReplicationForm';
describe('ReplicationForm component', () => {
it('calls saveUrl on submit with new url', () => {
const saveUrlFake = sinon.fake();
const otherFake = (url: string) => console.log(url);
render(<ReplicationForm saveUrl={otherFake} />, { wrapper: TestApp });
(screen.getByLabelText('Hostname') as HTMLInputElement).value = 'https://example.com';
(screen.getByLabelText('Database') as HTMLInputElement).value = 'poi';
(screen.getByLabelText('Username') as HTMLInputElement).value = 'shaw';
(screen.getByLabelText('Password') as HTMLInputElement).value = 'IMissYouR00t';
userEvent.click(screen.getByRole('button', { name: 'Submit' }));
console.log(saveUrlFake.callCount);
expect(saveUrlFake.lastCall.firstArg).to.equal('https://shaw:IMissYouR00t@example.com/poi');
});
});
当我使用 otherFake
时,回调被正确调用,所需的输出被打印到控制台。不幸的是,如果我使用 saveUrlFake
它永远不会被调用。 saveUrlFake.callCount
是 0
并且没有要检查的参数。
奇怪的是,当我用 saveUrlFake('https://shaw:IMissYouR00t@example.com/poi')
调用 saveUrlFake
时,测试成功了。我的问题是,为什么我的 sinon fake 没有被 React 组件调用?
tl;博士
有问题的完整组件:
import styled from '@emotion/styled';
import { Button, TextField, withTheme } from '@material-ui/core';
import React from 'react';
import { useForm } from 'react-hook-form';
import { FormattedMessage, useIntl } from 'react-intl';
const StyledForm = withTheme(
styled.form`
display: flex;
flex-direction: column;
& > *:not(div:first-of-type) {
margin-top: ${({ theme }) => theme.spacing(2)}px;
}
`
);
type FormModel = {
origin: string;
database: string;
username: string;
password: string;
};
type ReplicationFormProps = {
url?: string;
saveUrl: (url: string) => void;
};
export default function ReplicationForm({ url, saveUrl }: ReplicationFormProps) {
const intl = useIntl();
const getDefaultValuesFromURL = (inputUrl: string): FormModel => {
const { origin, username, password, pathname } = new URL(inputUrl);
return {
origin,
username,
password,
database: pathname.replaceAll('/', '')
};
};
const defaultValues: FormModel = url
? getDefaultValuesFromURL(url)
: {
origin: '',
database: '',
username: '',
password: ''
};
const { handleSubmit, formState, register } = useForm<FormModel>({ defaultValues });
const onSubmit = (data: FormModel) => {
const newUrl = new URL(data.origin);
newUrl.username = data.username;
newUrl.password = data.password;
newUrl.pathname = `/${data.database}`;
saveUrl(newUrl.toString());
};
return (
<StyledForm onSubmit={handleSubmit(onSubmit)}>
<TextField
id="replication-form-origin"
name="origin"
inputRef={register}
label={intl.formatMessage({ id: 'label.hostname', defaultMessage: 'Hostname' })}
fullWidth
/>
<TextField
id="replication-form-database"
name="database"
inputRef={register}
label={intl.formatMessage({ id: 'label.database', defaultMessage: 'Database' })}
fullWidth
/>
<TextField
id="replication-form-username"
name="username"
inputRef={register}
label={intl.formatMessage({ id: 'label.username', defaultMessage: 'Username' })}
fullWidth
/>
<TextField
id="replication-form-password"
name="password"
type="password"
inputRef={register}
label={intl.formatMessage({ id: 'label.password', defaultMessage: 'Password' })}
fullWidth
/>
<Button type="submit" color="primary" disabled={formState.isDirty}>
<FormattedMessage id="action.submit" defaultMessage="Submit" />
</Button>
</StyledForm>
);
}
回调好像是异步调用的,需要等待。测试库提供 waitFor()
,在这种情况下会有所帮助。
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect } from 'chai';
import * as sinon from 'sinon';
import TestApp from '../../views/App.test';
import ReplicationForm from './ReplicationForm';
describe('ReplicationForm component', () => {
it('calls saveUrl on submit with new url', async () => {
const saveUrlFake = sinon.fake();
render(<ReplicationForm saveUrl={saveUrlFake} />, { wrapper: TestApp });
userEvent.type(screen.getByLabelText('Hostname *') as HTMLInputElement, 'https://example.com');
userEvent.type(screen.getByLabelText('Database *'), 'poi');
userEvent.type(screen.getByLabelText('Username *'), 'shaw');
userEvent.type(screen.getByLabelText('Password *'), 'IMissYouR00t');
userEvent.click(screen.getByRole('button', { name: 'Submit' }));
await waitFor(() => expect(saveUrlFake.called).to.be.true);
expect(saveUrlFake.lastCall.firstArg).to.equal('https://shaw:IMissYouR00t@example.com/poi');
});
});