We are going to write two tests - one of the test will be brittle (it will test the implementation) and other test will be non-brittle (it will test the functionality).
App.js
import React, { Component } from 'react';
export default class Brittle extends Component{
state = {
number: 0
}
incrementNumber = () => {
const { number } = this.state;
this.setState({
number: number+1
})
}
render(){
const { number } = this.state;
return(
<div>
<div data-testid="increment-number">
{number}
</div>
<button data-testid="increment-button" onClick={this.incrementNumber}>
Increment Number
</button>
</div>
)
}
}
Here the functionality of component is that when the user clicks on button, the number is updated on screen.
The implementation is done using the state, calling the incrementNumber
function.
Here is an example of bad test, because if the developer changes the name of incrementNumber
function to incrementNo
, then the test is going to fail (Even though the functionality is not broken, the test fail unnecessarily).
it('should increment number - implementation test', () => {
const wrapper = shallow(<Brittle />);
wrapper.instance().incrementNumber();
expect(wrapper.state('number')).toEqual(1);
})
Following is the example of good test, which is independant of implementation.
it('should increment number - functionality test', () => {
const wrapper = shallow(<Brittle />);
expect(wrapper.find('[data-testid="increment-number"]').text()).toEqual('0')
wrapper.find('[data-testid="increment-button"]').simulate('click');
expect(wrapper.find('[data-testid="increment-number"]').text()).toEqual('1')
})
Let us test our assumption. We are going to change the name of function from incrementNumber
to incrementNo
. We are also going to change the name of state from number
to no
.
Our modified App.js
class is as follows
import React, { Component } from 'react';
export default class Brittle extends Component{
state = {
no: 0
}
incrementNo = () => {
const { no } = this.state;
this.setState({
no: no+1
})
}
render(){
const { no } = this.state;
return(
<div>
<div data-testid="increment-number">
{no}
</div>
<button data-testid="increment-button" onClick={this.incrementNo}>
Increment Number
</button>
</div>
)
}
}
Here are the result. The brittle test (first test) failed, while the second test passed.
Result for the failed brittle test
FAIL - should increment number - implementation test
TypeError: wrapper.instance(...).incrementNumber is not a function
16 | it('should increment number - implementation test', () => {
17 | const wrapper = shallow(<Brittle />);
> 18 | wrapper.instance().incrementNumber();
| ^
19 | expect(wrapper.state('number')).toEqual(1);
20 | })
The brittle test is not able to find the incrementNumber
function, as we have renamed it.
So you have understood how to write tests in a way which do not break when a future developer changes the code without changing the functionality of component. Its going to save your time writing unnecessary tests, and also save the future developer's time re-writing the failed tests.