Búsqueda de sitios web

reactjs - Pruebas de un Componente Funcional de React con Hooks usando Jest


Así que me estoy alejando de los componentes basados en clases hacia los componentes funcionales, pero estoy atascado al escribir pruebas con jest/enzyme para los métodos dentro de los componentes funcionales que usan explícitamente hooks. Aquí está la versión simplificada de mi código.

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;

Sé que puedo escribir pruebas para validateEmail exportándolo. Pero, ¿qué pasa con probar los métodos validateForm o handleSubmit? Si fuera un componente basado en clases, podría simplemente hacer un shallow del componente y usarlo desde la instancia como

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

Pero esto no funciona con los componentes funcionales ya que los métodos internos no se pueden acceder de esta manera. ¿Existe alguna forma de acceder a estos métodos o deberían los componentes funcionales ser tratados como una caja negra durante las pruebas?

Respuesta #1

En mi opinión, no deberías preocuparte por probar individualmente los métodos dentro del FC, sino por probar sus efectos secundarios. Por ejemplo:

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

Dado que podrías estar utilizando useEffect, que es asíncrono, podrías querer envolver tu expect en un setTimeout:

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

Otra cosa que podrías querer hacer es extraer cualquier lógica que no tenga nada que ver con interactuar con el formulario hacia funciones puras. por ejemplo: en lugar de:

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

Puedes refactorizar:

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

ComponenteDeInicioDeSesión.jsx

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

De esta manera podrías probar individualmente isPasswordValid y isEmailValid, y luego, al probar el componente Login, puedes simular tus importaciones. Y entonces, lo único que quedaría por probar en tu componente Login sería que al hacer clic, se llamen los métodos importados, y luego el comportamiento basado en esas respuestas, por ejemplo:

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

La principal ventaja de este enfoque es que el componente Login solo debería encargarse de actualizar el formulario y nada más. Y eso se puede probar de manera bastante directa. Cualquier otra lógica, debería manejarse por separado (separación de responsabilidades).

Respuesta #2

No se pueden escribir comentarios, pero debes notar que lo que dijo Alex Stoicuta está mal:

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

esta afirmación siempre pasará, porque ... nunca se ejecuta. Cuenta cuántas afirmaciones hay en tu prueba y escribe lo siguiente, porque solo se realiza una afirmación en lugar de dos. Así que revisa tus pruebas ahora por falsos positivos)

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

 expect(true).toEqual(true);

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

Respondiendo a tu pregunta, ¿cómo pruebas los hooks? No lo sé, estoy buscando una respuesta yo mismo, porque por alguna razón el useLayoutEffect no se está probando para mí...

Artículos relacionados