Así que me estoy alejando de los componentes basados en clases a los componentes funcionales, pero estoy atascado mientras escribo la prueba con broma/enzima para los métodos dentro de los componentes funcionales que usan ganchos explícitamente. 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 correo electrónico exportándolo. Pero, ¿qué hay de probar los métodos validateForm o handleSubmit ? Si se tratara de componentes basados en clases, podría simplemente reducir el componente y usarlo desde la instancia como
const wrapper = shallow(<Login />); wrapper.instance().validateForm()Pero esto no funciona con componentes funcionales ya que no se puede acceder a los métodos internos de esta manera. ¿Hay alguna forma de acceder a estos métodos o los componentes funcionales deben tratarse como una caja negra durante la prueba?
En mi opinión, no debería preocuparse por probar individualmente los métodos dentro del FC, sino por probar sus efectos secundarios. p.ej:
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ía estar usando useEffect, que es asíncrono, es posible que desee envolver su expectativa en un setTimeout :
setTimeout(() => { expect(submitButton.prop('disabled')).toBeTruthy(); });Otra cosa que podría querer hacer es extraer cualquier lógica que no tenga nada que ver con la interacción con las funciones puras de introducción de formulario. por ejemplo: en lugar de:
setIsLoginDisabled(password.length < 8 || !validateEmail(email));Puedes refactorizar:
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()) } import { isPasswordValid, isEmailValid } from './Helpers'; .... const validateForm = () => { setIsLoginDisabled(!isPasswordValid(password) || !isEmailValid(email)); }; .... De esta manera, podría probar individualmente isPasswordValid e isEmailValid , y luego, al probar el componente de inicio de Login , puede simular sus importaciones . Y luego, lo único que queda por probar para su componente de inicio de Login sería que al hacer clic, se llama a los métodos importados y luego el comportamiento basado en esa respuesta, 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 de inicio de Login solo debe manejar la actualización del formulario y nada más. Y eso se puede probar bastante sencillo. Cualquier otra lógica, debe manejarse por separado ( separación de preocupaciones ).
No puede escribir comentarios, pero debe tener en cuenta que lo que dijo Alex Stoicuta está mal:
setTimeout(() => { expect(submitButton.prop('disabled')).toBeTruthy(); });esta afirmación siempre pasará, porque... nunca se ejecuta. Cuente cuántas afirmaciones hay en su prueba y escriba lo siguiente, porque solo se realiza una afirmación en lugar de dos. Así que revise sus pruebas ahora para falsos positivos)
it('should fail',()=>{ expect.assertions(2); expect(true).toEqual(true); setTimeout(()=>{ expect(true).toEqual(true) }) }) Respondiendo a su pregunta, ¿cómo prueba los ganchos? No sé, busco una respuesta yo mismo, porque por alguna razón el useLayoutEffect no se está probando para mí ...
Entonces, tomando la respuesta de Alex, pude formular el siguiente método para probar el componente.
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), }); }); });Para probar las actualizaciones de estado como Alex mencionó, probé los efectos secundarios:
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', ); });pero para probar los ganchos del ciclo de vida sigo usando mount en lugar de superficial, ya que aún no es compatible con el renderizado superficial. Separé los métodos que no están actualizando el estado en un archivo de utilidades separado o fuera del componente React Function. Y para probar los componentes no controlados, configuré un accesorio de atributo de datos para establecer el valor y verifiqué el valor simulando eventos. También he escrito un blog sobre la prueba de los componentes de la función React para el ejemplo anterior aquí: https://medium.com/@acesmndr/testing-react-funcional-components-with-hooks-using-enzyme-f732124d320a