What I’ve learned about testing
March 07, 2019
Over the past few months, I have been testing a ton of React components with jest and enzyme. That has dramatically improved my testing skills not only in React, but in general. Here is what I’ve learned.
TL;DR: I’ve learned that tests are important, really important.
Always test behaviour
In the beginning, I used to have the goal of writing at least one unit test for each method of a class. That soon became a problem when I started to refactor things.
Whenever I modified the internal class methods, I had to modify the unit tests too. Then, one of my colleagues gave me a great piece of advice:
If you focus on testing behaviour, the tests are more likely to be valid after changing any code.
And he was damn right.
Another way of saying it: You should always test the public methods of your class (or the exported functions of your module). For example, in the case of a React component, it means that your unit tests shouldn’t directly access the component’s state. You should instead use its public methods/properties, that is, the props and the render method:
class Counter {
...
onClick = () => {
this.setState({ count: this.state.count + 1 });
}
getCount = () => this.state.count;
render() {
return (
<div>
<div className="count">Count: {this.getCount()}</div>
<button onClick={this.onClick} />
</div>
);
}
}
How should we test this? We could check the returned value from getCount after directly calling the onClick method. However, if we removed the getCount method in the future, that unit test would need to be modified too.
That’s why we should focus on testing behaviour:
// These unit tests wouldn't fail with CounterV2!
describe('Counter', () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<Counter />);
});
it('should show the current count', () => {
expect(wrapper.find('.count').text()).toBe('Count: 0');
});
it('should increase the counter after clicking on button', () => {
wrapper.find('button').simulate('click');
expect(wrapper.find('.count').text()).toBe('Count: 1');
});
});
class CounterV2 {
...
newFancyOnClick = () => {
this.setState({ count: this.state.count + 1 });
}
render() {
return (
<div>
<div className="count">Count: {this.state.counter}</div>
<button onClick={this.newFancyOnClick} />
</div>
);
}
}
As you can see, testing the actual behaviour of the component will save you a lot of time and effort when changing things in the future! 🎉
Note: One of things I miss in Javascript is access modifiers. When working with React + Typescript, I usually set as many methods as I can to private in order to force myself to not test them. You can’t test what you can’t access 🙃
Don’t trust code coverage
It definitely gives you a good overview of the tested code, but a 100% code coverage doesn’t mean that it is bulletproof. And that’s ironic because a lot of companies enforce their employees to increase these metrics when they don’t really matter.
Despite this fact, I do agree that a properly tested file is definitely going to have a 100(ish)% code coverage. For that reason, companies should aim for good quality tests instead of focusing on code coverage.
Tests are code too
So you have to make them clear, concise and readable. Ideally, any programmer should be able to understand how a piece of software works by looking at its tests.
The description of the unit test should be pithy, as well as the names of the variables you use in it. I always try to ask myself the same question:
Is the expected behaviour of this unit test completely defined in its description?
In addition, when reviewing tests, you should pay the same attention as you would pay if you were reviewing any new feature. Those tests are what you are going to hold on when you change something in the future.
Tests also need refactoring
The fact that your tests don’t fail after adding some new code doesn’t mean that you can ignore them.
Tests should be revised whenever you make a change on the code that they test. Following the rule of “Always test behaviour”, they should always be in sync with the current code behaviour.
You’ll thank them later
And probably sooner than you think. I can’t tell you how many times I’ve had this thought:
Phew, that would have passed unnoticed if we hadn’t had tests.
Yes, they can slow you down, but tests definitely worth the effort of writing them.
Conclusion
Even if you don’t like them, it’s essential to be aware of their importance. Tests are one of the pillars of reliable and maintainable software, and because of that, you should give them the same love you would give to any new cool feature.