The What and the How of Testing in React
Whether you’re an entrepreneur launching a service or a developer working on an application, one thing is for sure: your front end needs to impress and it needs to do it fast. In other words, if you want to succeed, the quality of the product you deliver needs to be impeccable and there’s little room for bugs. That’s why I do my best to include tests and have reliable test coverage on all front-end projects in Symphony.
First and most obviously, testing saves money and time. The software development lifecycle includes many phases and if bugs are discovered early on, it costs much less to fix them. However, solid testing will ensure the best product quality and customer satisfaction that keeps them coming back.
I never met a React developer who is really comfortable with writing tests.
Yet, tests are often omitted. Entrepreneurs, especially when just starting out, are already barely making ends meet, so it’s no surprise that they try to cut down the hours and workforce needed to ship the product. Some developers, on the other hand, keep pushing to work on new features, which can seem more exciting.
I get it, you want to get down to the fun part. But, considering the cost of fixing a bug found after product release can be 6-7x higher than for the one discovered in the testing phase, I always prefer to spend the extra time and effort on writing tests for each product we work on. While it may sound counterintuitive, this way I actually get to spend less time fixing bugs in the long run. Win!
Adopting the testing mindset within a team
When defining the project scope, it’s a common practice to define the testing strategy as well. However, if rules and procedures are not clearly defined by the client, our team as a whole discusses and defines the strategy for writing tests for the React app. This is to avoid common excuses such as certain features being seen as more important than test coverage, tests not being a requirement from the client-side, developers not having the time to write tests, or considering them difficult.
Once these obstacles have been removed, the team agrees on the testing library or framework, and then the discussion moves to test types.
Unit or integration / functional tests
Unit tests are usually the first step of testing because they are simple, have a few dependencies, fast execution, and are easy to write. Even though they are so simple, unit tests are tremendously helpful and reliable when written correctly. In addition to the fact that they can lead to 40%-80% reductions in production bug density, it’s easy to find the root of the problem if some of them fail.
While unit tests are a better choice for pure functions that consistently return the same results when given the same inputs, integration tests are more complex than unit and have a dependency on data, database, services, etc. They include multiple components or modules and require more time to write. They should be written after unit tests as they test the integration of multiple components.
So, where is the line between unit and integration testing? The difference can be blurry, but if we test small components and mock all dependencies ( isolation mode ) then we have a unit test, for sure. If we stop mocking data and dependencies, we’re moving to integration tests.
As a developer, I prefer writing both unit and integration tests, as long as we test application behavior. The component behavior must stay the same and the same test scenarios should pass - we’re testing application stability instead of implementation. For example, to display an error banner on the top of the UI, we used a React Context API, but later on, we found it can be better implemented with Observable. We changed implementation but error banner tests remained the same since we didn’t test the internal state and implementation, but inputs and UI outputs.
The behavior must stay the same and the same test scenarios should pass. So, we’re testing application stability instead of implementation.
Tip: We shouldn’t test an internal state since it can change but still, app behavior should remain the same. Writing tests this way, we should change tests only if business logic is changed.
What and how much should you test
High code coverage is not a guarantee we have a stable application in and of itself. Don’t think about reaching high code coverage; write tests that are meaningful and follow both the basic and edge-case scenarios. High test coverage may be achieved with render and snapshot testing, but in terms of test scenarios, it doesn't contribute much towards ensuring high quality.
The biggest challenge is what and how much we should test. A team should choose a library that follows best practices and is the best match for the application. Good documentation and test examples are also important since developers usually need some time to get comfortable with testing and a good source of information speeds up everything.
Which testing libraries/frameworks we should use?
Well, whatever you agree with your team. In the end, tests can be written with any popular frameworks or libraries. There are two most popular frameworks:
- React Testing Library - https://testing-library.com/docs/react-testing-library/intro
I won’t talk much more about these since I used both and I have a great experience. It’s up to you to check documentation, experiences, and make a decision on which one to use.
Tips and tricks
- Create a mock folder with all data from API calls you need for tests. That way you can easily find and import any mock data. If some API response changes you can change the mock in a single place and run tests again.
- Write render tests for small components to ensure the component is not broken
Since I use TypeScript and Lint, the probability of committing and pushing broken code is really low but I want to have it covered with tests
- Write unit tests for small components
I aim to cover component behavior based on props passed to it. I want to be sure props are used as requested and the user will see the correct behavior
- Write integration tests for a set of components
This is usually done for higher-order components, lists, and components that have some external dependencies, like buttons that will trigger data fetching, show loader, and then load data.
In the end, I find that the QA team is really important and helpful since they are responsible for writing end-2-end tests to test the whole application flow. That means they catch everything that the developer missed to test.
Author: ALMINA HASKIC, Software engineer with 6+ years of professional experience