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 make them mandatory on all front-end projects I work on 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 that keeps customers 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 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. Writing them correctly means not touching the internal state ( if testing component ) and testing all edge cases.
When a unit test is written, the behavior must stay the same and the same test scenarios should pass. So, we’re testing user perspective instead of implementation.
For example, we had to display an error banner on the top of the UI, and we used a React Context API, but later on, we found it can be better implemented with an observable pattern. 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.
While unit tests are a better choice for standalone components, integration tests are more complex and have a dependency on other components, data, services, etc. They include multiple components or modules and require more time to write them. With more complexity comes more value too. Integration tests will ensure that the application works correctly. With years of experience in writing tests, I found integration tests most useful in the early detection of bugs.
But, where is the line between unit and integration testing? The difference can be blurry, but if we test standalone components and mock all dependencies ( isolation mode ) then we have a unit test. On the other hand, integration tests include the interaction of multiple components, without mocking all data.
Tip: We shouldn’t test an internal state because the test will break if we change how we handle the state internally. This is where the react-testing-library is good.
What and how much should you test
One of the challenges 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.
High code coverage is not a guarantee we have a stable application in and of itself, but it’s an important factor if achieved correctly. We could simply reach high test coverage by writing snapshot tests and testing only happy path scenarios, which doesn’t get the product far. Instead, we should write tests that are meaningful and follow both the basic and edge-case scenarios.
Which testing libraries/frameworks we should use?
Well, whatever you agree with your team. In the end, tests can be written with any popular framework or library. There are two most popular frameworks/libraries:
- React Testing Library - https://testing-library.com/docs/react-testing-library/intro
- Enzyme - https://enzymejs.github.io/enzyme/
I used both and I have had a great experience. It’s up to you to check documentation, experiences, and make a decision that makes the most sense to you. But, I will mention that lately, I use react-testing-library since it doesn’t let me access the component state and mess with it. I just test user perspective, not thinking about implementation details.
Tips and tricks
- Think about user perspective - as developers, we usually think about implementation, but this time we should think like users.
- Test application stability instead of implementation.
- Think about edge case scenarios since they are hard to catch.
- Do not mock everything - writing integration tests with mocked dependencies can be tricky. It could, again, lead to testing a happy path scenario.
- Write render tests for small components to ensure the component is not broken.
- Write unit tests for standalone components and helper functions that you don’t expect to change very often and want to be sure they are not broken accidentally.
Test component behavior based on props passed to it. Be sure props are used as requested and the user will see the correct output.
- Write integration tests for a set of components to test the business logic.
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.
- When mocking service response, think about undefined or empty data.
- Create a mock folder with all the data from API calls that 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.
- Once a bug is found, make sure you write the test so it’s not repeated.
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.