使用Jest使用Hooks测试React功能组件

时间:2019-02-15 16:44:47

标签: reactjs typescript jestjs enzyme react-hooks

因此,我正在从基于类的组件转移到功能组件,但是在用笑话/酶为功能组件内部的方法(明确使用钩子)编写测试时陷入困境。这是我的代码的精简版。

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

但这不适用于功能组件,因为无法以这种方式访问​​内部方法。有什么方法可以访问这些方法,还是应该在测试时将功能组件视为黑盒?

5 个答案:

答案 0 :(得分:8)

我认为,您不必担心单独测试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组件时,可以mock your imports。然后只需测试一下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组件应只处理更新表单,而无需执行其他任何操作。可以直接测试。任何其他逻辑,都应单独处理separation of concerns)。

答案 1 :(得分:1)

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

下面是使用酶浅包装程序的示例,该浅包装程序将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()中。您可以使用相同的帮助程序,而无需进行任何修改。

答案 2 :(得分:0)

无法发表评论,但您必须注意Alex Stoicuta所说的是错误的:

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

此断言将始终通过,因为...从未执行。计算测试中有多少个断言并编写以下内容,因为仅执行一个断言而不是两个。因此,现在检查您的测试是否有误报

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

 expect(true).toEqual(true);

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

回答您的问题,如何测试挂钩?我不知道自己寻找答案,因为某种原因useLayoutEffect并未为我测试...

答案 3 :(得分:0)

因此,通过接受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功能组件外部的方法分开。 为了测试不受控制的组件,我设置了一个数据属性prop来设置值,并通过模拟事件检查了值。我也为上面的示例写了一个关于测试React Function组件的博客: https://medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a

答案 4 :(得分:0)

代替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>
</>);

当我尝试类似的事情并试图从测试用例中检查按钮的状态(启用/禁用)时,我没有获得该状态的预期值。但是我删除了disable = {isLoginDisabled}并替换为(password.length <8 ||!validateEmail(email)),它像一种魅力。 附注:我是React的初学者,所以对React的了解非常有限。