繁体   English   中英

使用 Jest 使用钩子测试 React 功能组件

[英]Testing React Functional Component with Hooks using 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编写测试。 但是如何测试validateFormhandleSubmit方法。 如果它是基于类的组件,我可以浅化组件并从实例中使用它作为

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

但这不适用于功能组件,因为无法通过这种方式访问​​内部方法。 有没有办法访问这些方法,或者在测试时应该将功能组件视为黑盒?

在我看来,您不应该担心在 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())
}

登录组件.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组件应该只处理更新表单而不处理其他任何事情。 这可以非常直接地进行测试。 任何其他逻辑,应单独处理关注点分离)。

不能写评论,但你必须注意 Alex Stoicuta 所说的是错误的:

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

这个断言将永远通过,因为......它永远不会被执行。 计算您的测试中有多少断言并编写以下内容,因为只执行一个断言而不是两个。 所以现在检查你的测试是否有误报)

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

 expect(true).toEqual(true);

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

回答你的问题,你如何测试钩子? 我不知道,自己在寻找答案,因为出于某种原因,我没有对useLayoutEffect进行测试...

因此,通过采用 Alex 的回答,我能够制定以下方法来测试组件。

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

为了测试 Alex 提到的状态更新,我测试了副作用:

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 而不是浅层渲染,因为浅层渲染尚不支持它。 我确实将不更新状态的方法分离到单独的 utils 文件或 React 函数组件之外。 为了测试不受控制的组件,我设置了一个数据属性道具来设置值并通过模拟事件检查该值。 我还写了一篇关于测试上面例子的 React 函数组件的博客: https : //medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a

目前 Enzyme 不支持 React Hooks,Alex 的答案是正确的,但看起来人们(包括我自己)正在努力使用 setTimeout() 并将其插入 Jest。

下面是使用 Enzyme 浅层包装器的示例,该包装器通过异步调用调用 useEffect() 钩子,从而导致调用 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() 中。 您可以使用相同的助手而无需任何修改。

而不是 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:我是 React 的初学者,所以对 React 的了解非常有限。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM