reactjs - Test ederken React bileşeni geri aramalarında sinon sahte kullanmak


Açıklayamadığım bir şeyle karşılaşıyorum. Aşağıdaki testi yaptım:

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:[email /poi');
  });
});

otherFake kullandığımda, geri çağırma doğru şekilde çağrılıyor ve istenen çıktı konsola yazdırılıyor. Ne yazık ki saveUrlFake kullandığımda, bu hiç çağrılmıyor. saveUrlFake.callCount 0 ve kontrol edilecek bir argüman yok.

Garip bir şekilde, testi kendim saveUrlFake('https://shaw:[email /poi') ile çağırdığımda çalışıyor. Sorum şu: React bileşeni tarafından sinon fake’im neden çağrılmıyor?

özet

Söz konusu olan tam bileşen:

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

Cevap #1

Görünüşe göre geri çağırma (callback) eşzamansız olarak çağrılıyor, bu nedenle beklenmesi gerekiyor. Test kütüphanesi, bu tür durumlarda yardımcı olan waitFor() yöntemini sunuyor.

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:[email /poi');
  });
});