Тестирование функционального компонента React с помощью крючков с помощью Jest - PullRequest
0 голосов
/ 15 февраля 2019

Таким образом, я перехожу от компонентов на основе классов к функциональным компонентам, но застрял при написании теста с помощью jest / энзима для методов внутри функциональных компонентов, в которых явно используются хуки.Вот урезанная версия моего кода.

function validateEmail(email: string): boolean {
  return email.includes('@');
}

const Login: React.FC<IProps> = (props) => {
  const [isLoginDisabled, setIsLoginDisabled] = React.useState<boolean>(true);
  const [email, setEmail] = React.useState<string>('');
  const [password, setPassword] = React.useState<string>('');

  React.useLayoutEffect(() => {
    validateForm();
  }, [email, password]);

  const validateForm = () => {
    setIsLoginDisabled(password.length < 8 || !validateEmail(email));
  };

  const handleEmailChange = (evt: React.FormEvent<HTMLFormElement>) => {
    const emailValue = (evt.target as HTMLInputElement).value.trim();
    setEmail(emailValue);
  };

  const handlePasswordChange = (evt: React.FormEvent<HTMLFormElement>) => {
    const passwordValue = (evt.target as HTMLInputElement).value.trim();
    setPassword(passwordValue);
  };

  const handleSubmit = () => {
    setIsLoginDisabled(true);
      // ajax().then(() => { setIsLoginDisabled(false); });
  };

  const renderSigninForm = () => (
    <>
      <form>
        <Email
          isValid={validateEmail(email)}
          onBlur={handleEmailChange}
        />
        <Password
          onChange={handlePasswordChange}
        />
        <Button onClick={handleSubmit} disabled={isLoginDisabled}>Login</Button>
      </form>
    </>
  );

  return (
  <>
    {renderSigninForm()}
  </>);
};

export default Login;

Я знаю, что могу написать тесты для validateEmail, экспортировав его.Но как насчет тестирования методов validateForm или handleSubmit?Если бы это были компоненты, основанные на классах, я мог бы просто обработать компонент и использовать его из экземпляра как

const wrapper = shallow(<Login />);
wrapper.instance().validateForm()

Но это не работает с функциональными компонентами, так как внутренние методы не могут быть доступны таким образом.Есть ли способ получить доступ к этим методам или функциональные компоненты должны рассматриваться как черный ящик во время тестирования?

Ответы [ 4 ]

0 голосов
/ 29 июля 2019

Вместо состояния isLoginDisabled попробуйте использовать функцию напрямую для отключения.Например,

const renderSigninForm = () => (
<>
  <form>
    <Email
      isValid={validateEmail(email)}
      onBlur={handleEmailChange}
    />
    <Password
      onChange={handlePasswordChange}
    />
    <Button onClick={handleSubmit} disabled={(password.length < 8 || !validateEmail(email))}>Login</Button>
  </form>
</>);

Когда я пытался подобным образом и пытался проверить состояние (включено / отключено) кнопки из тестового примера, я не получил ожидаемое значение для состояния.Но я удалил disabled = {isLoginDisabled} и заменил его на (password.length <8 ||! ValidateEmail (email)), это сработало как шарм.PS: я новичок в реагировании, поэтому у меня очень ограниченные знания по реакции. </p>

0 голосов
/ 07 июня 2019

Невозможно написать комментарии, но вы должны заметить, что то, что сказал Алекс Стойкута, неверно:

setTimeout(() => {
  expect(submitButton.prop('disabled')).toBeTruthy();
});

это утверждение всегда пройдет, потому что ... оно никогда не выполняется.Подсчитайте, сколько утверждений в вашем тесте, и напишите следующее, потому что выполняется только одно утверждение вместо двух.Итак, проверьте свои тесты сейчас на наличие ложных срабатываний)

it('should fail',()=>{
 expect.assertions(2);

 expect(true).toEqual(true);

 setTimeout(()=>{
  expect(true).toEqual(true)
 })
})

Отвечая на ваш вопрос, как вы тестируете хуки?Я не знаю, ищу ответ сам, потому что по какой-то причине useLayoutEffect не проверяется для меня ...

0 голосов
/ 11 июня 2019

Итак, взяв ответ Алекса, я смог сформулировать следующий метод для тестирования компонента.

describe('<Login /> with no props', () => {
  const container = shallow(<Login />);
  it('should match the snapshot', () => {
    expect(container.html()).toMatchSnapshot();
  });

  it('should have an email field', () => {
    expect(container.find('Email').length).toEqual(1);
  });

  it('should have proper props for email field', () => {
    expect(container.find('Email').props()).toEqual({
      onBlur: expect.any(Function),
      isValid: false,
    });
  });

  it('should have a password field', () => {
    expect(container.find('Password').length).toEqual(1);
  });

  it('should have proper props for password field', () => {
    expect(container.find('Password').props()).toEqual({
      onChange: expect.any(Function),
      value: '',
    });
  });

  it('should have a submit button', () => {
    expect(container.find('Button').length).toEqual(1);
  });

  it('should have proper props for submit button', () => {
    expect(container.find('Button').props()).toEqual({
      disabled: true,
      onClick: expect.any(Function),
    });
  });
});

Для проверки обновлений состояния, как упомянул Алекс, я проверял наличие побочных эффектов:

it('should set the password value on change event with trim', () => {
    container.find('input[type="password"]').simulate('change', {
      target: {
        value: 'somenewpassword  ',
      },
    });
    expect(container.find('input[type="password"]').prop('value')).toEqual(
      'somenewpassword',
    );
  });

, но для проверки ловушек жизненного цикла я все еще использую mount вместо поверхностного, поскольку он еще не поддерживаетсяв неглубоком рендеринге.Я отделил методы, которые не обновляют состояние, в отдельный файл утилит или вне компонента React Function.И для тестирования неконтролируемых компонентов я установил опору атрибута данных, чтобы установить значение, и проверил значение, имитируя события.Я также написал блог о тестировании компонентов функций React для приведенного выше примера здесь: https://medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a

0 голосов
/ 20 февраля 2019

По моему мнению, вам не следует беспокоиться об индивидуальном тестировании методов внутри FC, а скорее о тестировании его побочных эффектов.Например:

  it('should disable submit button on submit click', () => {
    const wrapper = mount(<Login />);
    const submitButton = wrapper.find(Button);
    submitButton.simulate('click');

    expect(submitButton.prop('disabled')).toBeTruthy();
  });

Поскольку вы можете использовать useEffect, который является асинхронным, вы можете захотеть обернуть свое ожидание в setTimeout :

setTimeout(() => {
  expect(submitButton.prop('disabled')).toBeTruthy();
});

Еще одна вещь, которую выможет захотеть сделать, это извлечь любую логику, которая не имеет ничего общего с взаимодействием с формой intro чистых функций.Например: вместо:

setIsLoginDisabled(password.length < 8 || !validateEmail(email));

Вы можете выполнить рефакторинг:

Helpers.js

export const isPasswordValid = (password) => password.length > 8;
export const isEmailValid    = (email) => {
  const regEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

  return regEx.test(email.trim().toLowerCase())
}

LoginComponent.jsx

import { isPasswordValid, isEmailValid } from './Helpers';
....
  const validateForm = () => {
    setIsLoginDisabled(!isPasswordValid(password) || !isEmailValid(email));
  };
....

Таким образомвы можете индивидуально протестировать isPasswordValid и isEmailValid, а затем при тестировании компонента Login вы можете смоделировать ваш импорт .И тогда единственное, что осталось проверить для вашего Login компонента, это то, что при щелчке будут вызваны импортированные методы, а затем поведение, основанное на этих ответах, например:

- it('should invoke isPasswordValid on submit')
- it('should invoke isEmailValid on submit')
- it('should disable submit button if email is invalid') (isEmailValid mocked to false)
- it('should disable submit button if password is invalid') (isPasswordValid mocked to false)
- it('should enable submit button if email is invalid') (isEmailValid and isPasswordValid mocked to true)

Основное преимущество этогоПодход заключается в том, что компонент Login должен обрабатывать , обновляя форму , и ничего больше.И это можно проверить довольно прямо.Любая другая логика должна обрабатываться отдельно ( разделение интересов ).

...