测试 React Hook 状态变化
修改 (2020):我建议切换到 React-testing-library,并改变你的测试方法,测试组件如何随状态变化,而不是状态本身的变化。虽然这种方法有效,但它并非解决问题的理想方案。我最初想出这个解决方案是因为我开始使用类组件进行 React 工作时,能够使用 Enzyme 助手直接测试状态值是否符合我的预期。有了 Hooks,测试方法就改变了,不再理想。我们应该测试整个组件以及组件如何响应状态变化,而不是状态本身发生了变化。
随着 React Hooks 的引入,测试组件状态变化不再像以前那么简单。不过,我们仍然可以直接测试这些状态变化,只需要一点 mocking 即可。🤠
使用类组件测试状态变化
以前,如果您使用 React 类组件,您可以简单地从浅层对象 Enzyme 通过浅层渲染向我们提供的内容中读取和操作组件状态。
import React from 'react'; | |
class TestComponent extends React.PureComponent { | |
constructor(props) { | |
super(props); | |
this.state = { count: 0 }; | |
} | |
render() { | |
return ( | |
<h3>{this.state.count}</h3> | |
<span> | |
<button id="count-up" type="button" onClick={() => this.setState({ count: this.state.count + 1 })}> | |
Count Up! | |
</button> | |
<button id="count-down" type="button" onClick={() => this.setState({ count: this.state.count - 1 })}> | |
Count Down! | |
</button> | |
<button id="zero-count" type="button" onClick={() => this.setState({ count: 0 })}>Zero</button> | |
</span> | |
) | |
} | |
} | |
export default TestComponent; |
import React from 'react'; | |
import Enzyme from 'enzyme'; | |
import Adapter from 'enzyme-adapter-react-16'; | |
import TestComponent from './ClassComponent'; | |
Enzyme.configure({ adapter: new Adapter() }); | |
describe('<TestComponent />', () => { | |
let wrapper; | |
beforeEach(() => { | |
wrapper = Enzyme.shallow(<TestComponent />); | |
}); | |
it('has the initial state count of zero', () => { | |
expect(wrapper.state()).toEqual({ count: 0 }); | |
}) | |
describe('The Count Up Button', () => { | |
it('increments state count by 1 on click', () => { | |
wrapper.find('#count-up').props().onClick(); | |
expect(wrapper.state()).toEqual({ count: 1 }); | |
}); | |
}); | |
describe('The Count Down Button', () => { | |
it('decrements state count by 1 on click', () => { | |
wrapper.find('#count-down').props().onClick(); | |
expect(wrapper.state()).toEqual({ count: -1 }); | |
}); | |
}); | |
describe('The Count Zero Button', () => { | |
it('sets state count to 0 on click', () => { | |
wrapper.setState({ count: 10 }); | |
wrapper.find('#zero-count').props().onClick(); | |
expect(wrapper.state()).toEqual({ count: 0 }); | |
}); | |
}); | |
}); |
import React from 'react'; | |
class TestComponent extends React.PureComponent { | |
constructor(props) { | |
super(props); | |
this.state = { count: 0 }; | |
} | |
render() { | |
return ( | |
<h3>{this.state.count}</h3> | |
<span> | |
<button id="count-up" type="button" onClick={() => this.setState({ count: this.state.count + 1 })}> | |
Count Up! | |
</button> | |
<button id="count-down" type="button" onClick={() => this.setState({ count: this.state.count - 1 })}> | |
Count Down! | |
</button> | |
<button id="zero-count" type="button" onClick={() => this.setState({ count: 0 })}>Zero</button> | |
</span> | |
) | |
} | |
} | |
export default TestComponent; |
import React from 'react'; | |
import Enzyme from 'enzyme'; | |
import Adapter from 'enzyme-adapter-react-16'; | |
import TestComponent from './ClassComponent'; | |
Enzyme.configure({ adapter: new Adapter() }); | |
describe('<TestComponent />', () => { | |
let wrapper; | |
beforeEach(() => { | |
wrapper = Enzyme.shallow(<TestComponent />); | |
}); | |
it('has the initial state count of zero', () => { | |
expect(wrapper.state()).toEqual({ count: 0 }); | |
}) | |
describe('The Count Up Button', () => { | |
it('increments state count by 1 on click', () => { | |
wrapper.find('#count-up').props().onClick(); | |
expect(wrapper.state()).toEqual({ count: 1 }); | |
}); | |
}); | |
describe('The Count Down Button', () => { | |
it('decrements state count by 1 on click', () => { | |
wrapper.find('#count-down').props().onClick(); | |
expect(wrapper.state()).toEqual({ count: -1 }); | |
}); | |
}); | |
describe('The Count Zero Button', () => { | |
it('sets state count to 0 on click', () => { | |
wrapper.setState({ count: 10 }); | |
wrapper.find('#zero-count').props().onClick(); | |
expect(wrapper.state()).toEqual({ count: 0 }); | |
}); | |
}); | |
}); |
使用钩子测试状态变化
然而,随着 hooks 的引入,你现在可以通过React.useState为函数式组件赋予状态。这意味着我们的 Enzyme 浅渲染对象将不再有state()方法。
我之前找到的关于这个主题的实现都谈到了测试状态变化的影响。例如,状态更新时,我们测试显示的计数值是否符合预期,或者测试函数是否使用来自状态的正确参数调用,等等。
我认为这是一种完全有效的测试状态变化的方法。然而,它似乎违背了单元测试时应该考虑的隔离原则。
如果我正在测试 onClick 事件,我真正关心的只是它是否调用了 setCount 并传入了它应该传入的变量。我们相信 React 能够正常工作;因此,我的测试不应该依赖于 useState 来更新状态变量并重新渲染组件以进行单元测试。
那么,我们为什么不嘲笑它呢?
import React from 'react'; | |
const TestComponent = () => { | |
const [count, setCount] = React.useState(0); | |
return ( | |
<h3>{count}</h3> | |
<span> | |
<button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button> | |
<button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button> | |
<button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button> | |
</span> | |
); | |
} | |
export default TestComponent; |
import React from 'react'; | |
import Enzyme from 'enzyme'; | |
import Adapter from 'enzyme-adapter-react-16'; | |
import TestComponent from './FunctionalComponent'; | |
Enzyme.configure({ adapter: new Adapter() }); | |
describe('<TestComponent />', () => { | |
let wrapper; | |
const setState = jest.fn(); | |
const useStateSpy = jest.spyOn(React, 'useState') | |
useStateSpy.mockImplementation((init) => [init, setState]); | |
beforeEach(() => { | |
wrapper = Enzyme.shallow(<TestComponent />); | |
}); | |
afterEach(() => { | |
jest.clearAllMocks(); | |
}); | |
describe('Count Up', () => { | |
it('calls setCount with count + 1', () => { | |
wrapper.find('#count-up').props().onClick(); | |
expect(setState).toHaveBeenCalledWith(1); | |
}); | |
}); | |
describe('Count Down', () => { | |
it('calls setCount with count - 1', () => { | |
wrapper.find('#count-down').props().onClick(); | |
expect(setState).toHaveBeenCalledWith(-1); | |
}); | |
}); | |
describe('Zero', () => { | |
it('calls setCount with 0', () => { | |
wrapper.find('#zero-count').props().onClick(); | |
expect(setState).toHaveBeenCalledWith(0); | |
}); | |
}); | |
}); |
import React from 'react'; | |
const TestComponent = () => { | |
const [count, setCount] = React.useState(0); | |
return ( | |
<h3>{count}</h3> | |
<span> | |
<button id="count-up" type="button" onClick={() => setCount(count + 1)}>Count Up</button> | |
<button id="count-down" type="button" onClick={() => setCount(count - 1)}>Count Down</button> | |
<button id="zero-count" type="button" onClick={() => setCount(0)}>Zero</button> | |
</span> | |
); | |
} | |
export default TestComponent; |
import React from 'react'; | |
import Enzyme from 'enzyme'; | |
import Adapter from 'enzyme-adapter-react-16'; | |
import TestComponent from './FunctionalComponent'; | |
Enzyme.configure({ adapter: new Adapter() }); | |
describe('<TestComponent />', () => { | |
let wrapper; | |
const setState = jest.fn(); | |
const useStateSpy = jest.spyOn(React, 'useState') | |
useStateSpy.mockImplementation((init) => [init, setState]); | |
beforeEach(() => { | |
wrapper = Enzyme.shallow(<TestComponent />); | |
}); | |
afterEach(() => { | |
jest.clearAllMocks(); | |
}); | |
describe('Count Up', () => { | |
it('calls setCount with count + 1', () => { | |
wrapper.find('#count-up').props().onClick(); | |
expect(setState).toHaveBeenCalledWith(1); | |
}); | |
}); | |
describe('Count Down', () => { | |
it('calls setCount with count - 1', () => { | |
wrapper.find('#count-down').props().onClick(); | |
expect(setState).toHaveBeenCalledWith(-1); | |
}); | |
}); | |
describe('Zero', () => { | |
it('calls setCount with 0', () => { | |
wrapper.find('#zero-count').props().onClick(); | |
expect(setState).toHaveBeenCalledWith(0); | |
}); | |
}); | |
}); |
结论
在这个实现中,我们模拟了 React.useState 并返回一个数组,其中包含传递给该方法的初始值和一个 jest 模拟函数。这会将状态设置器设置为我们的模拟函数,并允许我们测试它是否被调用并设置了预期的状态值。
太棒了!现在我们不用费力地翻找 props 来检查状态是否设置正确了。👌
鏂囩珷鏉ユ簮锛�https://dev.to/theactualgivens/testing-react-hook-state-changes-2oga