React Native has become one of the most popular development tools for creating top-notch mobile applications. Developing world-class products require substantial effort in achieving high-quality delivery.
The Agile methodology of delivering software products encourages quick delivery of features. Due to speedy delivery expectations, developers sometimes find themselves inclining towards developing error-prone code with buggy features, having functionality that works fine, but comes with a fragility that makes it difficult to extend the delivered work confidently.
A typical feature delivery process entails:
- Develop Product Specifications
- Breakdown Features in Epics
- Create Tasks in Project Management platforms like JIRA
- Meet and Discuss the specs and Tasks with Developers
- Task by Task Development of Features by Engineers
- Tasks move from Backlog to In progress, QA and Done
It is expected that once a task makes it to QA, the statement made by the developer should be quoted thus: “To the best of my knowledge, I have completed this feature, and it should function according to the specs.”
At this point, a QA Tester gets to test the feature and says, “Great Work! I will validate your claims and be sure this feature is well delivered because we can’t risk releasing our product with a malfunction”. The overall experience in software teams is to have a task stay in the QA bucket for a while because the features as delivered may lack the quality that the product demands to be accepted.
This could be a tiresome process for a team. However, it doesn’t have to be so. We can take a Test-driven development approach that allows us to be more critical with the code we write and the choices we make as we develop features.
Why take a Test-Driven Approach?
Uncover bugs more easily and earlier
The majority of bugs spring from edge cases that are not appropriately handled. Writing tests as we develop allows us to think about those edge cases and account for them upfront. Very troubling bugs can be avoided by taking some time to structure our implementations solidly enough to cater to the different possibilities in our code.
Save time and money
The process of testing is very repetitive. Tests can be run repeatedly at any time to reduce the length of time tasks spend with QA. Less time eventually is less money.
Help measure performance
Performance is a critical quality of a software product and an area that deserves special care. This is particularly true given that the attention span of users on applications is relatively short. Hence teams need to have optimized experiences and ensure the delivery of high or reasonably performant applications.
Provide some documentation
Testing allows us to describe the several features and use cases accounted for in a feature. Team members or other developers can gain some insight into the functionality of a feature by reading through the descriptions and assertions stated in the tests. This will make it easier for a contributor on the codebase to learn the likely impact of a change they intend to make to the project.
Achieve more reliability
By upholding a high standard of testing, we can be more confident that breaking changes can be caught more quickly, as tests will always be executed and will reveal unintended results. This improves the stability of a project to a drastic measure.
Reduce code complexity
When developing features, we often account for the testability of the code we write. This can force us to make a code much simpler due to how non-complicated we want to test. The impact of this is a more concise and declarative approach to programming, which yields easily maintainable codes and reduces technical debt.
Testing in React and React Native
These are the main types of testing we will discuss:
- Unit Testing
- Component Testing
- Integration Testing
- End-To-End (e2e) Testing
Unit testing involves the testing of individual units or components of a software system. With unit testing, small pieces of code that can be logically isolated in a system are validated based on their expected behavior. A unit may be a function, method, procedure, module, or object.
Unit testing often involves using “Mock Objects” created to test different sections of the code. Mocked objects are custom implementations of external objects or dependencies needed to fulfill tests.
An example will be the use of some asynchronous logic or network requests. We would provide mocks for the request calls within the tests because we shouldn’t be calling actual API endpoints from tests.
The code is quite readable, and the test describes the case that is being tested. In this case, we’re validating that after a record is created, it can be retrieved successfully. Ideally, the tests should include failure cases and invalid data cases to make for more reliability. The more edge cases we cover in the test, the more reliable test coverage we have.
Another example is presented below. Here we’re testing a simple Custom React Hook. There are few ideas to note here. The “getSingleEvent()” method exposed through this hook performs some caching. Let’s see how the test accounts for this below.
You will observe that the test cases captured here account for a successful case and didn’t stop there. To make for a qualitative test, we validate that after the first request is made, subsequent requests will pull the data from the cache, such that the “axios.get()” method is not called more than once, regardless of how many times we call the “getSingleEvent()” method. This works as long as the options in the argument are the same.
React/Native apps are made up of Components and form the actual interface a user interacts with to enjoy a product. To be more certain about the reliability of an app from a user’s standpoint, we can ensure to test the components adequately.
The goal is to validate the “rendering” and “interactions” accessible on the component.
On a form component that has a submit button, we can test that the component is rendered showing the form elements, change the content of the text input, have it reflect in the component state, or call an action on the click of the submit button displayed on the form.
These are closer to the actual interactions of a real user and will surely improve the quality of the developed feature, as it will be easier to catch breakages that occur due to refactors and feature updates.
The component above allows a user to add items to a shopping list.
The test simulates a “ChangeText” action, a button press, and goes on to assert that the UI reflects the desired change.
Component testing may involve some Snapshot Testing as well.
A component’s snapshot is a textual representation of the component’s render output generated during a test run. With snapshots, we can save the version of a component at a particular time. This helps avoid accidental changes to the UI. Text and style changes made have to be intentional so that changes are known whenever the snapshots are updated. For example, a plural word doesn’t accidentally become singular due to accidental removal of the trailing ‘s’.
Below are two images. The first shows a component, and the other gives an example of snapshot testing.
Integration testing combines multiple units of a product to test how they operate together in harmony. Integration tests are close to the most valuable kinds of tests we can have in a React app. It is more high-level and captures a more holistic validation of the app.
Integration tests strike an outstanding balance on the trade-offs between confidence and speed/expense of development. This is why it’s advisable to spend most of your effort there.
This captures a high-level test of the application from a user’s perspective. It simulates the process of opening up an app and interacting with the interface as a regular user would. This type of testing is not aware of the code or implementation details. It just expects that when a specific action is taken on the user interface, it yields a particular result on the interface.
Below are some examples of E2E tests:
There are several E2E testing tools available in the React Native community. Detox is a popular framework because it’s tailored for React Native apps.
Our confidence in the quality of products we release to users is greatly improved by these Testing methods. Even as these do not guarantee a 100% bug-free product, the Quality Assurance stage, which eventually involves manual testing, is not overburdened with much back and forth in testing. New features can be approved more quickly and get to users with minimal quality issues.