web-dev-qa-db-fra.com

Test React Composant fonctionnel avec des crochets en utilisant Jest

Je m'éloigne donc des composants basés sur les classes pour les composants fonctionnels mais je suis bloqué lors de l'écriture du test avec jest/enzyme pour les méthodes à l'intérieur des composants fonctionnels qui utilisent explicitement les crochets. Voici la version allégée de mon code.

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;

Je sais que je peux écrire des tests pour validateEmail en l'exportant. Mais qu'en est-il du test des méthodes validateForm ou handleSubmit. S'il s'agissait d'un composant basé sur une classe, je pourrais simplement superposer le composant et l'utiliser à partir de l'instance comme

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

Mais cela ne fonctionne pas avec les composants fonctionnels car les méthodes internes ne sont pas accessibles de cette façon. Existe-t-il un moyen d'accéder à ces méthodes ou les composants fonctionnels doivent-ils être traités comme une boîte noire lors des tests?

27
acesmndr

À mon avis, vous ne devriez pas vous soucier de tester individuellement les méthodes à l'intérieur du FC, plutôt de tester ses effets secondaires. par exemple:

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

Étant donné que vous utilisez peut-être useEffect qui est asynchrone, vous souhaiterez peut-être envelopper votre attente dans un setTimeout :

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

Une autre chose que vous voudrez peut-être faire, est d'extraire toute logique qui n'a rien à voir avec l'interaction avec les fonctions de pure intro de formulaire. par exemple: au lieu de:

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

Vous pouvez refactoriser:

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

De cette façon, vous pouvez tester individuellement isPasswordValid et isEmailValid, puis lors du test du composant Login, vous pouvez simuler vos importations . Et puis les seules choses qui restent à tester pour votre composant Login seraient que sur clic, les méthodes importées sont appelées, puis le comportement basé sur ces réponses, par exemple:

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

Le principal avantage de cette approche est que le composant Login doit juste gérer la mise à jour du formulaire et rien d'autre. Et cela peut être testé assez simplement. Toute autre logique, doit être traitée séparément ( séparation des préoccupations ).

30
Alex Stoicuta

Donc, en prenant la réponse d'Alex, j'ai pu formuler la méthode suivante pour tester le composant.

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

Pour tester les mises à jour d'état comme Alex l'a mentionné, j'ai testé les effets secondaires:

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

mais pour tester les crochets du cycle de vie, j'utilise toujours mount au lieu de shallow car il n'est pas encore pris en charge dans le rendu peu profond. J'ai séparé les méthodes qui ne mettent pas à jour l'état dans un fichier utils séparé ou en dehors du composant de fonction React. Et pour tester les composants non contrôlés, j'ai défini un accessoire d'attribut de données pour définir la valeur et vérifié la valeur en simulant des événements. J'ai également écrit un blog sur le test React Function Components pour l'exemple ci-dessus ici: https://medium.com/@acesmndr/testing-react -composants-fonctionnels-avec-crochets-utilisant-enzyme-f732124d320a

2
acesmndr

Actuellement, Enzyme ne prend pas en charge React Hooks et la réponse d'Alex est correcte, mais il semble que les gens (y compris moi-même) avaient du mal à utiliser setTimeout () et à le brancher sur Jest.

Vous trouverez ci-dessous un exemple d'utilisation du wrapper peu profond Enzyme qui appelle le hook useEffect () avec des appels asynchrones qui entraînent l'appel de hooks useState ().

// This is helper that I'm using to wrap test function calls
const withTimeout = (done, fn) => {
    const timeoutId = setTimeout(() => {
        fn();
        clearTimeout(timeoutId);
        done();
    });
};

describe('when things happened', () => {
    let home;
    const api = {};

    beforeEach(() => {
        // This will execute your useEffect() hook on your component
        // NOTE: You should use exactly React.useEffect() in your component,
        // but not useEffect() with React.useEffect import
        jest.spyOn(React, 'useEffect').mockImplementation(f => f());
        component = shallow(<Component/>);
    });

    // Note that here we wrap test function with withTimeout()
    test('should show a button', (done) => withTimeout(done, () => {
        expect(home.find('.button').length).toEqual(1);
    }));
});

De plus, si vous avez imbriqué des descriptions avec beforeEach () qui interagit avec le composant, vous devrez également encapsuler les appels beforeEach dans withTimeout (). Vous pouvez utiliser le même assistant sans aucune modification.

1
dimka

Impossible d'écrire des commentaires mais vous devez noter que ce que Alex Stoicuta a dit est faux:

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

cette assertion passera toujours, car ... elle n'est jamais exécutée. Comptez le nombre d'assertions dans votre test et écrivez ce qui suit, car une seule assertion est effectuée au lieu de deux. Vérifiez donc vos tests maintenant pour les faux positifs)

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

 expect(true).toEqual(true);

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

Répondant à votre question, comment testez-vous les crochets? Je ne sais pas, je cherche moi-même une réponse, car pour une raison quelconque, le useLayoutEffect n'est pas testé pour moi ...

0
John Archer

Au lieu de l'état isLoginDisabled, essayez d'utiliser la fonction directement pour désactivé. Par exemple.

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

Lorsque j'essayais quelque chose de similaire et que j'essayais de vérifier l'état (activé/désactivé) du bouton à partir du scénario de test, je n'ai pas obtenu la valeur attendue pour l'état. Mais j'ai supprimé disabled = {isLoginDisabled} et l'ai remplacé par (password.length <8 ||! ValidateEmail (email)), cela a fonctionné comme un charme. P.S: Je suis un débutant avec React, donc j'ai des connaissances très limitées sur React.

0
shoan