The talk will be presented as a refactoring story - will start from the messy untestable component, cover it with a brittle smoke test, and then show how to move all our react component logic into a custom hook and test this hook. Will present patterns to test things like - useState, effects, and Apollo.
Testing React Hooks with Confidence
Hello, everybody. My name is Radoslav Stankov. You can find me online over here. I'm head of engineering at Product Hunt and currently I'm located in Bulgaria. If you want to check out my presentations, they're over here. All the slides and code are linked to this page, so you can check them later. So today we're going to talk about testing hooks with confidence. And there is this big divide between test-driven development and automated testing, as you might see during this presentation. In my talk, I'm just going to mention how to automate testing. I won't go to test-driven and stuff around that. So again, talk is cheap and I don't have much of it, so let's show you some code. So I'm going to show you this untested react application. This is just a small calculator app. And this is the code of the app, and if you notice, there is a lot of it. And it's pretty simple app, but there is a lot of code. So how would we test, how do we know that it works? So what we can start with is we can start with just a simple smoke test. So for the smoke test, we can just add those test data attributes. So we can select the elements, we can use the react testing library, and we can create this very simple smoke test where we get the react testing library. Usually when I do tests, I like to have those helpers because overall, I hate writing five functions copying each other. So I just say, OK, click this data ID. And for my test, this is another small trick I do. If I need to rename the component, I don't want to go back and rename the test. So here, I just created a very simple smoke test. It just works. So I'm just testing.Empty zeroes, clicking plus one, one, plus two, remove, two, evaluate, and reset. Basically making sure that this component works. This is something that I would do normally to test if things work manually. This is just a placeholder. This is not like a real good test. I'm going to remove this in the end, but this just makes me sure when I refactoring, I don't break something. It's not a very good test because it doesn't follow the four phases of good testing. So where should we start now? So the first thing we can do is to clean up the code, what we can do is refactoring. I like to call extract custom hooks. This is one of my favorite features of hooks is that you can actually do hooks from other hooks. And they create these very nice interfaces. So here, all these big fat logic can just be extracted into a simple hook. And this hook can give you, okay, that's what's the memory of the calculator and what are the actions. And again, the code for the hook is just copy paste. It works. So how are we going to test this custom hook? Like this is the next step we needed to do. So for testing this custom hook, there is this nice library called react hooks for me up testing library, which allows you to very easy test the hook. And what I like to do is I want to have a function called emit hook, which basically hides the boilerplate of setting up the hook. And for example, I want to test the add action. So for the add action, it's just setting up the hook, adding card value and checking that after the hook is reloaded, we have updated our value. And we can basically test, okay, if I have two digits, they're added. And you can go through all the tests for add. For remove is very similar. You set up the hook, you just call it. You have this nice call function where you encapsulate this and you have all of this.
And again, it's a bit long and I would link you to the slides in the end so you can dig through more. And now when we have this test, which very thoroughly tests our hook with all its edge cases, with all its workarounds, we can actually start refactoring this hook. The thing we are going to end up in the end from this big ball of mud is basically the fact that this hook to use just a simple use reducer, where we say, okay, we have a reducer function and in the end, we have the same actions, add, remove, reset, evaluate. And this is again, the reducer. It's a bit more complex. It's a bit more split, but this actually tests our calculator. And we actually have like a full test coverage. And again, the code for this presentation is living in this address. So you can click it and go to the details. I actually split it into steps so you can see all the refactoring steps. And a couple of the final notes I want to say here is in the end, I'm actually removing all that code from the smoke test. I don't need it. What I have found in practice is usually when I write react components, they're either very dumb, like they get some data and render stuff. So it doesn't much value adding to test them as a unit test or the biggest logic and the biggest thing I have in my test is the hook code. Like usually that's the place where a lot of the complexity of my system is. So I generally stopped doing react unit tests. And what I try to do on the unit test level is to just unit test the simple function. Like for example, I can just test the reducer. I can just test the hook by itself and not test the react components. The way I'm sure that the react components work is through doing end to end testing with something like cypress or something like Capybara, which actually test my core workflow. Because I very rarely have found bugs in my system which were regarded to just the react components itself. Usually the bugs come from very complex hooks or when we have class components, class components. So yeah, that's basically what I wanted to share with you today and thank you for joining me.