Testing Your React Native App ( Part-1 )
By: Kartik Sharma
Tags: Testing, React Native

Guidelines For Better Testing

These are some of the guidelines I learned after researching how other developers are testing their code. Following these guidelines made it easier for me to write and maintain tests throughout the codebase.

1. Don’t test the implementation of your code

To understand why this is important, let’s go through a scenario. Suppose you wrote a function that takes n as an input and outputs the result of adding the first n numbers.

Let’s assume that you are testing getSumOfFirstN for n=2 by asserting that add is called 2 times with the arguments (0, 2) and (2, 1) respectively and the output of getSumOfFirstN is 3 .

Later, you find out that there is a better way to find the sum of first n numbers and rewrite the above code.

Your test will start breaking because you are no longer calling add

This was a straightforward example, and that’s why it might seem counter-intuitive, but I have seen a lot of developers doing exactly this kind of testing where the mistake is not so easy to see.

2. Don’t let your tests become another tech-debt

Testing is important, but you also need to be delivering the code to the relevant stakeholders on time. If whenever you write a new feature or modify an existing one, it breaks your old test cases or you end up spending a significant amount of your allocated time on fixing old test cases rather than building the required features, then ultimately you will end up deleting all the old tests and risk the project to fail in production.

Following these guidelines should help you in ensuring that your tests are either easily fixable or don’t break at all when your codebase changes.

3. When testing UI, write tests in a way that simulates actual user behaviour.

Suppose you are testing whether this button was rendered and working as expected or not then first think about how the user would find and interact with this button. I am hoping that they would see the “Submit” text and then press on it and so that’s exactly what we would simulate. In our test case, we will first search for the text “Submit” and then simulate the “onPress” event on it and then assert whatever that was supposed to do.

In some cases, it might not be easy to uniquely identify the component that you want to target, and in that case, you can use testID prop but remember that you will not be simulating the full user behaviour if you use testID because users don’t target components based on their testID .

Why is this important? Why must we try to simulate user behaviour as much as possible when testing UI? This is because in the end, it will be a human that will be interacting with your UI and not a computer and if they see a “Hello” being rendered in your button rather than a “Submit”, your test case should be failing because that is something that can throw the end user off.

4. Pure functions are easier to test than impure functions

Pure functions are functions that will always give the same output for a corresponding input, i.e. if a pure function pumps out 2 for a 1 then it would always do that whereas impure functions might pump-out 2 on the first call and then pump-out 5 on the second call.

This is handy to keep in mind when writing code. Impure functions can become easier to test if the module that is introducing the “impurity” in such functions is mockable.

5. Use fixtures as input and assertions in your tests

Suppose you have an object of type employee and you have various functions operating on them such as a function to capitalize the name , a function to check whether the employee is an adult or not, etc.

Now, suppose you take this object as input in all your test cases.

This is your dummy data or your “fixture”. In your first test case that tests whether the function that capitalizes the name is working as expected or not, you assert that it’s output is equal to { ...employee, name: employee.name.toUpperCase() } and in your second test case, you assert whether the function outputs employee.age >= 18 or not, and so on.

You might be wondering what advantage do we get by using fixtures in such a way? The answer is that you are making it easier for yourself to fix tests quickly in the future by writing your tests this way.

E.g. What if we want to add another property maritalStatus in the employee object because now all our employees are required to disclose their marital status. If in our first test case, we asserted the output to be equal to { name: "KARTIK", age: 25, sex: "Male", children: 0 } instead of { ...employee, name: employee.name.toUpperCase() } , our test case will break. It will also break if we change the value of name to something else. In other words, our test case would not be flexible at all and hence will have a higher chance of breaking due to unrelated changes in the codebase.

6. Write unit tests for components and utility functions

These are parts of your code that are going to be used by multiple modules. Hence, they need to be tested for all possible inputs / edge-cases because you don’t know how the other modules are going to be using these functions/components. Hence, these should not have any unexpected behaviour.

7. Write integration tests for screens

It’s hard to write unit tests for screens because usually, these depend on a large number of components and other third party libraries like a redux store. So, to write unit tests for screens, you will first have to mock all these dependencies, which is a lot of work. This is why it’s better to write integration tests in this case.

7. Write E2E tests for testing native code

Native code does not run in a jest environment. So to test it, you will have to use a library like Detox.

8. Always do snapshot testing for screens and components

If you make a style change in a component that is being used by multiple other screens/components, then the snapshot tests for those will fail until you update the snapshots. It will help you understand which other modules were affected by the change that you made. If you are working in a team, it really helps the reviewer during PR reviews understand what modules were affected by the particular change you made in the component because the changes in the snapshots of those modules are reflected in your PR.