Reactjs 使用Jest使用挂钩测试React功能组件

Reactjs 使用Jest使用挂钩测试React功能组件,reactjs,typescript,jestjs,enzyme,react-hooks,Reactjs,Typescript,Jestjs,Enzyme,React Hooks,因此,我将从基于类的组件转移到功能组件,但在为显式使用钩子的功能组件内的方法编写jest/Ezyme测试时,我陷入了困境。这是我的代码的精简版本 function validateEmail(email: string): boolean { return email.includes('@'); } const Login: React.FC<IProps> = (props) => { const [isLoginDisabled, setIsLoginDisab

因此,我将从基于类的组件转移到功能组件,但在为显式使用钩子的功能组件内的方法编写jest/Ezyme测试时,我陷入了困境。这是我的代码的精简版本

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;

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

在我看来,您不应该担心单独测试FC内部的方法,而应该测试它的副作用。 例如:

您可能要做的另一件事是提取与form intro pure函数交互无关的任何逻辑。 如: 而不是:

setIsLoginDisabled(password.length < 8 || !validateEmail(email));
通过这种方式,您可以单独测试
isPasswordValid
isEmailValid
,然后在测试
登录
组件时,您可以。然后,您的
登录
组件剩下的唯一要测试的事情就是单击,调用导入的方法,然后基于这些响应的行为 例如:


这种方法的主要优点是,
登录
组件应该只处理更新表单,而不处理其他事情。这可以直接测试。任何其他逻辑,都应单独处理。

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

setTimeout(() => {
  expect(submitButton.prop('disabled')).toBeTruthy();
});
此断言将始终通过,因为。。。它从未被执行过。计算测试中有多少断言并编写以下内容,因为只执行一个断言而不是两个。因此,现在检查您的测试是否为假阳性)


回答你的问题,你如何测试钩子?我不知道,我自己在寻找答案,因为出于某种原因,
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),
    });
  });
});
但是为了测试生命周期挂钩,我仍然使用mount而不是shall,因为它在浅层渲染中还不受支持。 我将不更新状态的方法分离到一个单独的utils文件或React函数组件之外。 为了测试非受控组件,我设置了一个数据属性prop来设置值,并通过模拟事件来检查值。我还写了一篇关于测试上述示例的React函数组件的博客:

请尝试直接将该功能用于禁用状态,而不是isLoginDisabled状态。 例如

const rendersigninfo=()=>(
登录
);
当我尝试类似的事情并试图从测试用例中检查按钮的状态(启用/禁用)时,我没有得到状态的预期值。但是我删除了disabled={isLoginDisabled}并将其替换为(password.length<8 | | |!validateEmail(email)),它就像一个符咒一样工作。
注:我是react的初学者,所以对react的了解非常有限。

目前酶不支持react挂钩,Alex的答案是正确的,但看起来人们(包括我自己)正在努力使用setTimeout()并将其插入笑话中

下面是一个使用Ezyme浅包装器调用useEffect()钩子的示例,异步调用会导致调用useState()钩子

//这是我用来包装测试函数调用的助手
const with timeout=(完成,fn)=>{
const timeoutId=setTimeout(()=>{
fn();
clearTimeout(timeoutId);
完成();
});
};
描述('事情发生的时间',()=>{
让我们回家;
常量api={};
在每个之前(()=>{
//这将在组件上执行useffect()钩子
//注意:您应该在组件中使用React.useffect(),
//但不包括带有React.useffect导入的useffect()
spyOn(React,'useffect').mock实现(f=>f());
组件=浅();
});
//注意,这里我们用withTimeout()包装测试函数
测试('应该显示一个按钮',(完成)=>withTimeout(完成,()=>{
expect(home.find(“.button”).length.toEqual(1);
}));
});

另外,如果您有与组件交互的beforeach()的嵌套描述,那么您还必须将beforeach调用包装到withTimeout()中。您可以使用相同的帮助程序,无需任何修改。

我删除了我的旧答案,因为它是错误的,对不起。。。顺便说一句:是的,功能组件是黑匣子,而测试并不能回答这个问题。一旦你有足够的声誉,你将能够评论任何帖子;相反,提供不需要提问者澄清的答案。@John尝试安装而不是使用酶来测试生命周期方法。似乎测试
useffect
产生的效果的唯一方法是通过
setTimeout
函数。我希望Ezyme能提供一个稍微更直观的DSL来等待异步效果完成,但似乎还没有什么。覆盖率如何处理这样的测试?内部功能/挂钩是否会标记为已覆盖?大多数人都同意,但有一个很大的挑战。登录组件的唯一职责是公开登录行为(而不仅仅是更新表单)。我不会提取特定于登录到另一个文件并在测试中模拟这些导入行为的逻辑(可以自由提取,但不要模拟)。通过这样做,您将登录组件的测试与其实现紧密耦合,从而导致测试脆弱性(对提取的逻辑进行任何重构都可能破坏测试,即使行为没有改变)。测试登录的行为,而不是实现!我可以确认,不测试FC内部的功能会损害某些应用程序强制要求的代码覆盖率。最简单的方法是将函数移出i
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));
  };
....
- 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)
setTimeout(() => {
  expect(submitButton.prop('disabled')).toBeTruthy();
});
it('should fail',()=>{
 expect.assertions(2);

 expect(true).toEqual(true);

 setTimeout(()=>{
  expect(true).toEqual(true)
 })
})
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',
    );
  });
const renderSigninForm = () => (
<>
  <form>
    <Email
      isValid={validateEmail(email)}
      onBlur={handleEmailChange}
    />
    <Password
      onChange={handlePasswordChange}
    />
    <Button onClick={handleSubmit} disabled={(password.length < 8 || !validateEmail(email))}>Login</Button>
  </form>
</>);