typescript 使用 Jest 使用钩子测试 React 功能组件
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/54713644/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Testing React Functional Component with Hooks using Jest
提问by acesmndr
So I'm moving away from class based components to functional components but am stuck while writing test with jest/enzyme for the methods inside the functional components which explicitly uses hooks. Here is the stripped down version of my code.
因此,我正在从基于类的组件转移到功能组件,但是在使用 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;
I know I can write tests for validateEmail
by exporting it. But what about testing the validateForm
or handleSubmit
methods. If it were a class based components I could just shallow the component and use it from the instance as
我知道我可以validateEmail
通过导出它来编写测试。但是如何测试validateForm
orhandleSubmit
方法。如果它是基于类的组件,我可以浅化组件并从实例中使用它作为
const wrapper = shallow(<Login />);
wrapper.instance().validateForm()
But this doesn't work with functional components as the internal methods can't be accessed this way. Is there any way to access these methods or should the functional components be treated as a blackbox while testing?
但这不适用于功能组件,因为无法通过这种方式访问内部方法。有没有办法访问这些方法,或者在测试时应该将功能组件视为黑盒?
回答by Alex Stoicuta
In my opinion, you shouldn't worry about individually testing out methods inside the FC, rather testing it's side effects. eg:
在我看来,您不应该担心在 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();
});
Since you might be using useEffect which is async, you might want to wrap your expect in a setTimeout:
由于您可能正在使用异步的 useEffect,您可能希望将您的期望包装在setTimeout 中:
setTimeout(() => {
expect(submitButton.prop('disabled')).toBeTruthy();
});
Another thing you might want to do, is extract any logic that has nothing to do with interacting with the form intro pure functions. eg: instead of:
您可能想做的另一件事是提取与与表单介绍纯函数交互无关的任何逻辑。例如:而不是:
setIsLoginDisabled(password.length < 8 || !validateEmail(email));
You can refactor:
您可以重构:
Helpers.js
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
登录组件.jsx
import { isPasswordValid, isEmailValid } from './Helpers';
....
const validateForm = () => {
setIsLoginDisabled(!isPasswordValid(password) || !isEmailValid(email));
};
....
This way you could individually test isPasswordValid
and isEmailValid
, and then when testing the Login
component, you can mock your imports. And then the only things left to test for your Login
component would be that on click, the imported methods get called, and then the behaviour based on those response
eg:
通过这种方式,您可以单独测试isPasswordValid
和isEmailValid
,然后在测试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)
The main advantage with this approach is that the Login
component should just handle updating the formand nothing else. And that can be tested pretty straight forward. Any other logic, should be handled separately(separation of concerns).
这种方法的主要优点是Login
组件应该只处理更新表单而不处理其他任何事情。这可以非常直接地进行测试。任何其他逻辑,应单独处理(关注点分离)。
回答by acesmndr
So by taking Alex's answer I was able to formulate the following method to test the component.
因此,通过采用 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),
});
});
});
To test the state updates like Alex mentioned I tested for sideeffects:
为了测试 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',
);
});
but to test the lifecycle hooks I still use mount instead of shallow as it is not yet supported in shallow rendering. I did seperate out the methods that aren't updating state into a separate utils file or outside the React Function Component. And to test uncontrolled components I set a data attribute prop to set the value and checked the value by simulating events. I have also written a blog about testing React Function Components for the above example here: https://medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a
但是为了测试生命周期钩子,我仍然使用 mount 而不是浅层渲染,因为浅层渲染尚不支持它。我确实将不更新状态的方法分离到单独的 utils 文件或 React 函数组件之外。为了测试不受控制的组件,我设置了一个数据属性道具来设置值并通过模拟事件检查该值。我还在这里写了一篇关于测试 React 函数组件的博客:https: //medium.com/@acesmndr/testing-react-functional-components-with-hooks-using-enzyme-f732124d320a
回答by dimka
Currently Enzyme doesn't support React Hooks and Alex's answer is correct, but looks like people (including myself) were struggling with using setTimeout() and plugging it into Jest.
目前 Enzyme 不支持 React Hooks,Alex 的答案是正确的,但看起来人们(包括我自己)正在努力使用 setTimeout() 并将其插入 Jest。
Below is an example of using Enzyme shallow wrapper that calls useEffect() hook with async calls that results in calling useState() hooks.
下面是一个使用 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);
}));
});
Also, if you have nested describes with beforeEach() that interacts with component then you'll have to wrap beforeEach calls into withTimeout() as well. You could use the same helper without any modifications.
此外,如果您使用与组件交互的 beforeEach() 嵌套描述,那么您还必须将 beforeEach 调用包装到 withTimeout() 中。您可以使用相同的助手而无需任何修改。
回答by shoan
Instead of isLoginDisabled state, try using the function directly for disabled. Eg.
而不是 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>
</>);
When I was trying similar thing and was trying to check state(enabled/disabled) of the button from the test case, I didn't get the expected value for the state. But I removed disabled={isLoginDisabled} and replaced it with (password.length < 8 || !validateEmail(email)), it worked like a charm. P.S: I am a beginner with react, so have very limited knowledge on react.
当我尝试类似的事情并尝试从测试用例中检查按钮的状态(启用/禁用)时,我没有得到状态的预期值。但是我删除了 disabled={isLoginDisabled} 并将其替换为 (password.length < 8 || !validateEmail(email)),它就像一个魅力。PS:我是 React 的初学者,所以对 React 的了解非常有限。
回答by John Archer
Cannot write comments but you must note that what Alex Stoicuta said is wrong:
不能写评论,但你必须注意 Alex Stoicuta 所说的是错误的:
setTimeout(() => {
expect(submitButton.prop('disabled')).toBeTruthy();
});
this assertion will always pass, because ... it's never executed. Count how many assertions are in your test and write the following, because only one assertion is performed instead of two. So check your tests now for false positive)
这个断言将永远通过,因为......它永远不会被执行。计算您的测试中有多少断言并编写以下内容,因为只执行一个断言而不是两个。所以现在检查你的测试是否有误报)
it('should fail',()=>{
expect.assertions(2);
expect(true).toEqual(true);
setTimeout(()=>{
expect(true).toEqual(true)
})
})
Answering your question, how do you test hooks? I don't know, looking for an answer myself, because for some reason the useLayoutEffect
is not being tested for me...
回答你的问题,你如何测试钩子?我不知道,自己在寻找答案,因为出于某种原因,useLayoutEffect
没有为我测试......