Every time I start working on a new project, I get goosebumps whenever I find a large untested feature. Code that is not tested, can’t be trusted. It is until we dig into the full code path of the feature, that we can know for sure what it does, what dependencies does it have and what does it need to work right.
While it is true that once we reach a medium-to-large/large scale application, we can get to spend a considerable amount of time maintaining unit tests. Just imagine that we have +95% of code coverage with unit tests. That would mean we have a similar amount of lines of code for our unit tests. We are basically maintaining two applications. And that is not a bad thing! On the contrary, we can be certain that we do not break any isolated functionality when something changes.
TDD with unit tests can help us develop faster, avoid debugging, get better abstractions, etc. We test each piece of code in isolation, to make it work the way we intended to… But what if we change the way it works?
Imagine we have a suite of React components for a TO-DO list. We could have something like this:
Now, at first, we think it would be a good idea to use
TaskManager to hold our local state. So we do the following:
- We create our onChange & onClick callbacks everywhere.
- We tie up every component with the TaskManager
- We add validation logic in the TaskManager
- Then we add API communication in our TaskManager
Finally we unit test everything. If you are used to
enzyme and testing for props, you validate everything on isolation. And finally, we kindly ask our code: “Does this work?” we should get a few dozen “Yes” micro-answers.
But what if one day we need the state to be handled by the redux store? It’s not a complicated task itself. So we change everything, but what happens to our unit tests? We ask our code again “Does this work?” the answer is an explosive “NO!”. So what went wrong?
I mean, we tried it out in the browser and it worked perfectly, so why we get a “NO!”? shouldn’t the answer still be the same?
Yes, indeed, it should. But somehow we asked a different question. We asked a hundred of micro-questions “does this work if I use it this way?”.
Imagine if we wanted to move our validation logic to a
TaskService, or a saga. What if the API request was made elsewhere? What if the component’s props change in some way? We need to exhaustively fix all our spec files to match something that we know for sure it already works!
So What Should We Do?
So should we stop writing unit tests? ABSOLUTELY NOT! We need to know what, when and why to test, depending on the case. Some “code coverage” percentage does not directly represent how trustworthy your codebase is. Sometimes, it can even get to be counterproductive.
Unit tests are a great way to develop with confidence, reach for edge cases hard to reproduce, and many other benefits. But we can fill the gap it leaves with other types of tests, like integration.
Integration testing is the phase in software testing in which individual software modules are combined and tested as a group - Source
So, instead of testing “does this component have this prop?”, we can test “when a task is created, will it be rendered as expected?”. Now, that sounds like a better question to answer!
Kent C. Dodds, wrote a full article explaining good testing practices, explaining this tweet, and also made a great presentation, that it’s really worth to watch:
Okay, You Got My Interest
Great! That means you are interested in improving the quality of your code! You can make sure that, no matter what any clumsy developer changes in the future, your feature will still work as intended. So, you end up with a robust, maintainable, pride-worthy piece of code.
“But how much additional effort does it require? Are integration tests harder to develop than unit tests?”
It all depends on the case. Sometimes unit tests require a lot of mocking functions, and API responses and other stuff. And every time we mock, we strive further away from what the code actually does. So we can end up maintaining duplicated code that does not really work in such a way.
When we make integration tests, we can still change the redux state and the component’s props, but we test for the code’s behavior in such cases. So we have some flexibility, with a more powerful tool.
Can You Provide An Example?
That’s why I’m here! just take a look at this “counter” example. We have:
- A redux store
- 5 containers
- 4 Actions
- 1 Reducer
- 1 Selector
But do we test each and every file and declaration individually? Nope. We test only the feature. Notice how all of the above wast fully tested in only 58 lines of code. We actually achieved 100% coverage for an entire feature in just one file!
redux-integration-test-example - CodeSandbox
The online code editor tailored for web applications
Now, I’m aware this isn’t a complex feature. But it is an example of how easy it can be to get started with integration testing, and how little amount of code can be required to write robust tests.
Even if the example I provided isn’t the best, my goal here is to ignite the spark of curiosity in your heads saying that maybe, just maybe, there is a better way to improve the quality of your code.