因此,我正在从基于类的组件转移到功能组件,但是在用笑话/酶为功能组件内部的方法(明确使用钩子)编写测试时陷入困境。这是我的代码的精简版。
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()
但这不适用于功能组件,因为无法以这种方式访问内部方法。有什么方法可以访问这些方法,还是应该在测试时将功能组件视为黑盒?
答案 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));
您可以重构:
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));
};
....
这样,您可以分别测试isPasswordValid
和isEmailValid
,然后在测试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的了解非常有限。