web-dev-qa-db-ja.com

テストReact Jestを使用したフック付きの機能コンポーネント

したがって、クラスベースのコンポーネントから機能コンポーネントに移動していますが、明示的にフックを使用する機能コンポーネント内のメソッドについて、jest/enzymeでテストを作成しているときに行き詰まっていました。これが私のコードの削除されたバージョンです。

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

ただし、内部メソッドはこの方法でアクセスできないため、これは機能コンポーネントでは機能しません。これらのメソッドにアクセスする方法はありますか、またはテスト中に機能コンポーネントをブラックボックスとして扱う必要がありますか?

27
acesmndr

私の意見では、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();
});

あなたがしたいかもしれないもう一つのことは、イントロピュア関数のフォームとの相互作用とは何の関係もないロジックを抽出することです。例:代わりに:

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

この方法で、isPasswordValidisEmailValidを個別にテストできます。次に、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コンポーネントがフォームの更新だけを処理し、それ以外は何も処理しないことです。そして、それはかなり簡単にテストすることができます。その他のロジックは個別に処理する必要があります関心の分離 )。

30
Alex Stoicuta

したがって、アレックスの答えをとることによって、コンポーネントをテストするために次の方法を策定することができました。

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

しかし、ライフサイクルフックをテストするには、浅いレンダリングではまだサポートされていないため、浅い代わりにマウントを使用します。状態を更新していないメソッドを別のutilsファイルまたはReact関数コンポーネントの外に分離しました。そして、制御されていないコンポーネントをテストするために、値を設定するためにデータ属性プロップを設定してチェックしましたイベントをシミュレートすることによる値。上記の例のテストReact関数コンポーネントについてのブログもここに書きました: https://medium.com/@acesmndr/testing-react -functional-components-with-hooks-using-enzyme-f732124d320a

2
acesmndr

現在、EnzymeはReactをサポートしていません。フックとAlexの答えは正しいですが、(私も含めて)人がsetTimeout()を使用してJestに接続するのに苦労しているようです。

以下は、useEffect()フックを呼び出すEnzyme浅いラッパーを非同期呼び出しで使用して、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);
    }));
});

また、コンポーネントと対話するbeforeEach()で記述をネストしている場合は、beforeEach呼び出しをwithTimeout()にもラップする必要があります。同じヘルパーを変更せずに使用できます。

1
dimka

コメントを書くことはできませんが、アレックス・ストイクタが言ったことは間違っていることに注意する必要があります:

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

...決して実行されないため、このアサーションは常に成功します。 2つのアサーションではなく1つのアサーションのみが実行されるため、テストに含まれるアサーションの数を数えて次のように記述します。ですから、今すぐテストで偽陽性をチェックしてください)

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

 expect(true).toEqual(true);

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

あなたの質問に答えて、フックをどのようにテストしますか?どういうわけかuseLayoutEffectは私のためにテストされていないので、私は自分で答えを探しています。

0
John Archer

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:私は反応の初心者なので、反応に関する知識は非常に限られています。

0
shoan