What do we mean by Unit Testing?
With unit testing, our primary goal is to test all the smallest individual units or components that combine to become a part of the application/service. This enables us to bring confidence in the code that we’re writing. So, it is essential to test all the small units of our code in isolation and gain trust in the internal workings of the code along with checking all the various edge cases that may be present. Just to reiterate, we don’t focus on how multiple small units of the component act when they’re combined with other components. That comes under Integration Testing and is a different strategy of testing which we will go over in later modules. While testing, consider your code as a Black Box and forget how it is implemented, what we are concerned about is the result it returns based on various inputs we provide it.
Just for starters, consider the following add function —
const add = (x, y) => x + y;
Now as a smart developer, when adding this to your codebase you may think that -
Whoa! This will be used in multiple areas of our codebase, why not add this to a common utility file so that it can be reused and DRY is followed
Right, now since it will be used in a lot of places of your codebase it is your responsibility to cover all the cases of your code in a test. Even if it may be as simple as above, why so?
- In the future, if its implementation is changed slightly, the test cases would break, notifying the developer that what you’ve changed doesn’t pass the acceptance criteria of the codebase.
- It allows the users to get an idea of what all different cases can be there and those can be quickly documented as tests.
- When your atomic components/utilities are not accurate then how can one expect their bigger functionality to be bug-free?
- Unexpected edge cases/scenarios due to code errors do not arise in production causing downtime of the codebase.
- And the best reason would be while making small changes here and there you won’t necessarily need to spin up your application and verify the changes. (⚠️ While this is not recommended, you may need to decide on the amount of changes there are)
I don’t think that reasons for needing unit tests need to go on much more as the rest of the factors may depend on your organization’s take if they wish to spend resources on adding tests to secure and test your code in isolation.
But this brings us to another important part -
What do we test and what not?
Well, there are 2 scenarios whether the stuff you’re testing is a logical operation like let’s say a utility function, or if what you test is a reusable UI component like a Button, Textbox, etc.
- When testing a function, in most cases what you’d want is the function is pure (A pure function depends on its parameters only and doesn’t use data variables from out of the function’s scope). The reason is that if your function is usually calling global variables then you can’t isolate its functionality and your function becomes unpredictable (In the case of modern UI frameworks, we have pure components instead). Now coming back to what to test in the case of a functional code, we want to test a function’s input and output, test different edge cases and if there are if-else statements, we will be testing for both scenarios. If we have default values in the function’s parameter we will test for that too. This will help us gain confidence that we’ve tested all possible scenarios through which this function can be used.
- When testing a UI component, you would need to check whether the static texts are being properly shown, you can even check whether clicking or interacting with the component is working as expected and whether all props with all supported values give predictable results. It is a common misconception that developers wish to test the states and setState functions, you don’t want to test that since states are just a way of managing data in react, at the end of the day you’re more concerned about what you are showing to the user, irrespective of what kind of values the states hold.
The mental model while writing tests should be that tests are written in a way which resembles how the user is using your application. You’re not testing the limitations and capabilities of your framework, instead what business logic you’re using and what kind of user centric.
How is Unit Testing different from other forms of testing?
Consider the following twitch’s main dashboard UI -
If we analyze this from the POV of a front-end engineer, we can break the following into multiple pieces —
- Left Sidebar - Title with closing sidebar component - Streamer cards showing what they’re streaming and their user count
- Right Content Section - List of different recommendations which has title and preview cards - The preview cards have a thumbnail with different meta information regarding the stream. - Now the thumbnail will be a separate component while its information will be a separate component. They are brought together to make a complex component but with unit testing, we’re breaking components into small reusable UI elements.
- Navbar - It has the logo for Twitch. - It has a search bar. Now here you need to think of it as Twitch has its UI search bar which accepts props that are used to show the results of a query the user has written. You don’t make API calls from the SearchBar UI component but you would be using the reusable component separate from business logic in a container which will further make API calls and pass down the results to the component. SearchBar is a pure component, so we would keep it that way. - Multiple links to navigate users to different sections. - CTA buttons for sign-up and login.
- Sticky Footer - It has an image, text, and action button which can be a single component.
From the above exercise, we’ve broken down components into smaller reusable elements in our UI which are “dead components” i.e. they don’t interact with business logic or API but they show data they receive from props.
Even in Unit Testing, what you’d want is to test the most atomic components or logic in isolation from the rest of your components since you don’t care much about how it works when different components are brought together.
There must be a question, that you would’ve as to how would we test the working of an actual application in the way a user would use it. Like searching in the actual Twitch website, clicking on results then getting redirected to another section, basically an entire interaction. Well, that’s not what Unit testing is for that is E2E testing which tests your app, and its happy paths and emulates how would a user use your application. Unit testing as we have much more grasp on is to test the atomic components so that once they’re brought together you expect how it would work on its own.
Consider your code as a black box where you give it some inputs and get an output. You can give different kinds of inputs, sometimes 2, 3 and can vary its type and form and check the output against it. That’s unit testing.
I hope this introduction to Unit testing was understandable and has made things clear. It wasn’t very technical but we would be going over how we do unit testing with React using actual code in the next article. Until then, happy testing!