3 Levels of Mocking a React Hook: Control and Effort against Representability
June 29, 2021 - 9 minutes
Hooks are one of the most important aspects of any React project. Whether big or small, whether it is custom or not, every hook is responsible for a piece of logic and interactivity of the frontend application. Because of this, it is all the more important that they are dealt with correctly in frontend tests. But there are different ways to mock a React hook, all of which have different advantages and drawbacks.
Over the years, I have encountered this issue numerous times. A lot of the questions that come with it are: how should I deal with a React hook in my frontend test? Should I mock it or not? Is it even possible to mock it? How should I mock it? How much effort does mocking the hook require? Should I mock the entire hook or should I mock only certain parts of the hook? How does it impact the representability of my tests?
The interesting thing is that despite this set of questions remaining the same every time and every scenario feeling similar to the previous one, the answers to this set of questions were always slightly different. This meant that the solution would also be slightly different every time. In all of them, the considerations that made up these slight differences were also always regarding the same two factors in my experience.
On one axis, there is the control related to mocking the React hook. This describes the amount of control that the developer has over mocking the hook. The more control the developer has over the mocked hook, the more they can influence its behaviour and outcome in tests. Obviously, it is preferred to have as much control as possible from a development perspective, as that provides the most possibilities. But the amount of control goes hand-in-hand with the amount of effort that is required by the developer to deal with the hook. Having more control over the hook in tests means that the developer needs to consider more options, have a better understanding of the use cases, and do more to handle it properly.
On the other axis, there is the representability of the resulting test. This describes how realistic of a reflection our tests are of real end-user experience. Depending on how a React hook is mocked in the testing environment, different approaches can affect the representability of our tests in various ways. The higher the representability of our tests, the more it means that the tests are a realistic reflection of end-user experience, the more we can trust the test results to tell us whether a feature is broken or not, and thus the more value the tests provide.
Based on my experience, these axes were always opposite to each other. This meant that a solution that provided the developer with a lot of control and effort over mocking the hook would result in a test with relatively low representability. Vice versa, making sure that a test had very high representability of the actual user experience would require an approach that left me with little control and effort.
As ideally, we would maximise both axes, every time the consideration would boil down to balancing these two factors. Between the control and effort of mocking the hook and representability of the resulting test, which aspect are we willing to sacrifice for the other and how much?
In this article, I will go over the different ends of the spectrum and describe the different considerations that come with it. The purpose is to provide you with a clear understanding of this balancing act and the considerations that come with it. Using this, you can apply these considerations yourself the next time you are thinking about what the best approach is to mock a React hook and enhance the quality of your tests.
All of this is also framework agnostic. So whether you are working with Jest, Enzyme, Mocha, Jasmine, React Testing Library, another testing library, or any combination of the previous, you will still be able to apply what you will learn from this article to create more quality solutions to mocking hooks in React tests.
Mock The Entire hook
The most drastic measure to deal with React hooks in tests is to mock them away entirely. From a development perspective, this is the simplest approach, requires the least considerations and effort, and provides the most control. There are several ways to technically implement this, but the most straightforward approach would be something along the lines of:
// ComponentWithCustomHook.test.jsx
jest.mock("./hooks", () => ({
useCustomHook: () => { customString: "some-string", customCallback: jest.fn() },
}))
What we are doing is mocking the entire hooks
module and overwriting it with our implementation in the test environment. In this case, we replace the useCustomHook
export with an anonymous function that returns some dummy values. Whenever the custom hook is now called inside our tests, it will always return the dummy values that we provided.
There are several ways to diverge from this implementation based on your library and needs, like saving mocks for verifications, mocking a third-party library, and so on. But the concept behind all of them remains the same, namely that we want to mock the entire hook and control its behaviour entirely in our tests.
This approach provides the most control from the developer’s perspective. All you have to worry about is what the hook should return to your components in your tests. You do not have to worry about how the custom hooks work internally — no matter how complex the inner state, whether any network requests are performed, what dependencies it has internally, or whatever is done inside the hook, you do not have to care about that in any way as that will be mocked away. You can configure exactly how the hook influences different test scenarios by tweaking the mock. If you want to verify happy paths, then you can make the mock return exactly what you expect from the original hook in those scenarios. And the same applies to verifying unhappy paths. The control is entirely yours.
The biggest sacrifice with this approach is made in terms of the representability of the resulting tests. From a user perspective, this is the least representative of how users would interact with your application. While you gain simplicity, time, and control over the mock’s behaviour, you are actively diminishing the amount of actual code that your tests go through. Instead, strong assumptions are made regarding the input, the logic, and the expected output of the hook. In turn, the credibility of your tests is dependent on how valid these assumptions are.
But no matter how strong these assumptions are, mocking a hook still means you are getting rid of an essential part of your front end. Thus, when opting for this approach it is very important to consider whether you really need all this much control and the gained time and effort. Because of it, you are sacrificing a lot of the representability of your tests. In certain scenarios where the hook does not significantly affect the experience of the users, this can be a reasonable decision to make. But in a lot of other cases, this rarely applies.
Only Mock The Internals Of The hook
Another option to deal with a React hook in your frontend tests is to not mock the hook itself but to only mock certain internals of the hook. Prime candidates for this are interactions with external resources that determine the internal behaviour of the hook, like API calls. You could also think of expensive or complex calculations or the usage of code from third-party libraries.
Mocking the internals of React hooks will provide you with more fine-grained control over the result of those parts of code, but still, leave your part of the React hook untouched. Control and effort-wise, this is like a middle ground as this approach sacrifices a bit in both aspects compared to mocking the entire hook
Rather than controlling the entire hook, you now only control a part of it. Effort wise, you now have to dive into the internals of the hook and figure out how it works before you can properly mock them. In certain cases, this can require quite some additional time and effort. The most common case would be if you are dealing with hooks that were not written by you but rather by other parties, like third party libraries or other teams.
While you lose some points on the axis of control and effort, you gain some of it back on the representability one. Compared to mocking the entire hook, you are now only cutting off your React hook from reality at certain parts of the code. This means that you leave the other code in the hook untouched. Oftentimes, those are responsible for handling how your hook and components behave based on the results of those internals. Since you are not mocking those away anymore, your tests become a more realistic representation of how users would also perceive it during usage.
This approach is the biggest grey area on the spectrum between the two axes. A lot of the scenarios that you will come across will fall into this area. This area is also where most trade-offs are considered between the two axes and most minor solutions originate. It is a constant optimization process between how much representability can be sacrificed for control and effort, and vice versa how much control is worth the effort and necessary to justify the loss of representability.
Leave The Hook Untouched
On the other side of the spectrum compared to mocking the entire hook, there is also the option to not mock the hook at all. From a representability perspective, leaving the hook entirely untouched is the most preferred way of dealing with it. It is most similar to what end-users will experience while using your application, which is the best-case scenario for a testing environment. Applying this approach will maximise the representability of your tests.
However, these benefits do not come for free. While the representability greatly benefits from this approach, you will have to sacrifice a lot of control that you have of the hook in your tests. In fact, all of it as you are not touching the hook at all and are relying on the production behaviour. But this is basically what we want right, a testing environment that exactly matches our production environment so the rest results accurately match whether the features are broken for our end-users?
Well, not quite.
In certain cases, this approach is an infeasible or impossible way of dealing with React hooks. Performing network requests to an external API is a common occurrence that falls into this category. Not even considering realistic factors like API request limits, allowing your code to perform network requests in tests can introduce non-deterministic behaviour. This can in turn lead to the same tests having different results between test runs based on external factors that are out of your control, also known as being flaky tests. This is exactly not what you want from your tests.
In an ideal situation, our test environment would be an exact reflection of our production environment. Then, our tests would also be an exact reflection of how our application is working for our end-users, assuming tests are implemented properly. This approach tries to create such a situation, but unfortunately, in practice, it is not a realistic one. Depending on a lot of different factors, our test environment can’t be an exact reflection of our production environment without additional effort that is outside the scope of this article.
On the rare occasions that it is possible to leave a hook entirely untouched without any impact on the effort and the representability of your test, it is recommended to do so due to the importance of representability. But in the majority of the cases, it is important to consider whether sacrificing so much control is worth the gained representability and also the effort that potentially comes with it. Instead, sacrificing a small and reasonable amount of representability could result in a lot of control and saved effort, which is a more realistic decision to make in certain scenarios.
Final Thoughts
This article looked at three different approaches to mocking a React hook along the spectrum of the two axes that they consider. The first one being control that we have and the effort that we have to put in as developers, while in the opposite direction there is the representability of our tests compared to end-user scenarios. All described approaches are balancing between these two axes in different proportions. Mocking the entire React hook and leaving the hook untouched are on the outer ends of the spectrum for respectively control and effort, and representability. For both ends, there are scenarios in which they have their use cases, but those are less common.
In the middle of the spectrum, there is the approach of only mocking the internals and certain parts of the hook. This is an enormous grey area where a lot of small considerations can be made according to the different scenarios, which is the reason why similar cases can lead to different solutions. Based on the details, there are a lot of different ways in which mocking a React hook can be done. In the end, the most important thing to remember is that it is a balancing act on the spectrum between control and effort against representability: how much are you willing to give up and how much are you willing to reasonably sacrifice?