DEV Community

Cover image for Ensuring the Quality of Your App with Testing in React Native
Paulo Messias
Paulo Messias

Posted on

Ensuring the Quality of Your App with Testing in React Native

Hey devs!

Developing a robust, functional, and bug-free mobile application is a significant challenge. As a specialist in mobile development using React Native, one of my priorities is to ensure that every line of code works as expected. To achieve this, I apply various types of tests that cover everything from small functions to the complete integration of the application. In this post, we'll explore in detail the different types of tests I use and how they contribute to the quality of the app.

1. Unit Tests

Unit tests are the foundation of the testing pyramid. They focus on testing small, isolated parts of the code, such as functions or methods. In React Native, I use Jest, which is a highly efficient JavaScript testing framework.

Setting up Jest in React Native:

First, we need to set up Jest in our project. Let's install Jest and some necessary dependencies:

npm install --save-dev jest @testing-library/react-native @testing-library/jest-native
Enter fullscreen mode Exit fullscreen mode

In the package.json file, add the Jest configuration:

"jest": {
  "preset": "react-native",
  "setupFilesAfterEnv": ["@testing-library/jest-native/extend-expect"]
}
Enter fullscreen mode Exit fullscreen mode

Unit Test Example:

Let's create a simple function and write a test for it. Create a math.js file with the following function:

// math.js
export function sum(a, b) {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Now, create a test file math.test.js:

// math.test.js
import { sum } from './math';

test('sum of 1 and 2 equals 3', () => {
  expect(sum(1, 2)).toBe(3);
});

test('sum of negative numbers', () => {
  expect(sum(-1, -2)).toBe(-3);
});
Enter fullscreen mode Exit fullscreen mode

These tests check if the sum function returns the expected results. Unit tests help quickly detect errors during development and ensure that each part of the code works correctly in isolation.

2. Component Tests

Component tests ensure that React Native components function as expected. I use @testing-library/react-native to test the rendering and behavior of components.

Component Test Example:

Let's create a simple component MyComponent.js:

// MyComponent.js
import React from 'react';
import { Text, View } from 'react-native';

const MyComponent = () => (
  <View>
    <Text>Component Text</Text>
  </View>
);

export default MyComponent;
Enter fullscreen mode Exit fullscreen mode

Now, create a test file MyComponent.test.js:

// MyComponent.test.js
import React from 'react';
import { render } from '@testing-library/react-native';
import MyComponent from './MyComponent';

test('renders correctly', () => {
  const { getByText } = render(<MyComponent />);
  expect(getByText('Component Text')).toBeTruthy();
});
Enter fullscreen mode Exit fullscreen mode

In this test, we verify that the MyComponent component renders the text correctly. Component tests are essential to check the user interface and ensure that components react correctly to props and state.

3. Integration Tests

Integration tests verify that different parts of the application work well together. In React Native, I use @testing-library/react-native to perform these tests.

Integration Test Example:

Let's create two components MainScreen.js and DetailsScreen.js with navigation between them using React Navigation.

// MainScreen.js
import React from 'react';
import { Button, View, Text } from 'react-native';

const MainScreen = ({ navigation }) => (
  <View>
    <Text>Main Page</Text>
    <Button title="Go to details" onPress={() => navigation.navigate('Details')} />
  </View>
);

export default MainScreen;

// DetailsScreen.js
import React from 'react';
import { View, Text } from 'react-native';

const DetailsScreen = () => (
  <View>
    <Text>Details Screen</Text>
  </View>
);

export default DetailsScreen;
Enter fullscreen mode Exit fullscreen mode

To test the navigation, we need to set up a testing environment that supports React Navigation.

Integration Test Setup:

Install the necessary dependencies:

npm install @react-navigation/native @react-navigation/stack react-native-screens react-native-safe-area-context
Enter fullscreen mode Exit fullscreen mode

Set up an App.js that uses React Navigation:

// App.js
import 'react-native-gesture-handler';
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import MainScreen from './MainScreen';
import DetailsScreen from './DetailsScreen';

const Stack = createStackNavigator();

const App = () => (
  <NavigationContainer>
    <Stack.Navigator initialRouteName="Main">
      <Stack.Screen name="Main" component={MainScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  </NavigationContainer>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, create the integration test App.test.js:

// App.test.js
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import App from './App';

test('navigation between screens', async () => {
  const { getByText } = render(<App />);
  fireEvent.press(getByText('Go to details'));
  await expect(getByText('Details Screen')).toBeTruthy();
});
Enter fullscreen mode Exit fullscreen mode

In this test, we verify that the "Go to details" button navigates correctly to the DetailsScreen. Integration tests are vital to ensure that navigation flows and interaction between components are working correctly.

4. End-to-End (E2E) Tests

End-to-end tests simulate user behavior, verifying the application as a whole from start to finish. I use Detox for E2E testing in React Native.

Setting up Detox:

First, install Detox and set up our project:

npm install --save-dev detox
npx detox init -r jest
Enter fullscreen mode Exit fullscreen mode

Then, configure the package.json to include Detox scripts:

"scripts": {
  "test:e2e": "detox test",
  "build:e2e": "detox build"
}
Enter fullscreen mode Exit fullscreen mode

Configuring Detox for Android:

To configure Detox for Android, we need to adjust some configuration files and add build scripts.

First, add the Detox configuration in detox.config.js:

// detox.config.js
module.exports = {
  testRunner: 'jest',
  runnerConfig: 'e2e/config.json',
  configurations: {
    "android.emu.debug": {
      "binaryPath": "android/app/build/outputs/apk/debug/app-debug.apk",
      "build": "cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug && cd ..",
      "type": "android.emulator",
      "device": {
        "avdName": "Pixel_2_API_30"
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Configuring build.gradle:

In the android/app/build.gradle file, add the following block to configure Detox:

android {
  ...
  defaultConfig {
    ...
    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    multiDexEnabled true
  }
}

dependencies {
  ...
  androidTestImplementation 'com.wix:detox:+'
  androidTestImplementation 'androidx.test:runner:1.3.0'
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
Enter fullscreen mode Exit fullscreen mode

Configuring Detox for iOS:

To configure Detox for iOS, we need to adjust some configuration files and add build scripts.

First, add the Detox configuration in detox.config.js:

// detox.config.js
module.exports = {
  testRunner: 'jest',
  runnerConfig: 'e2e/config.json',
  configurations: {
    "ios.sim.debug": {
      "binaryPath": "ios/build/Build/Products/Debug-iphonesimulator/YourApp.app",
      "build": "xcodebuild -workspace ios/YourApp.xcworkspace -scheme YourApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build",
      "type": "ios.simulator",
      "device": {
        "type": "iPhone 12"
      }
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Configuring the Podfile:

In the ios/Podfile, add the following block to configure Detox:

target 'YourApp' do
  # Detox specific
  pod 'Detox', :path => '../node_modules/detox/ios'
end
Enter fullscreen mode Exit fullscreen mode

After configuring the Podfile, run the command:

cd ios && pod install
Enter fullscreen mode Exit fullscreen mode

E2E Test Example:

Let's create an E2E test file example.e2e.js:

// example.e2e.js
describe('E2E Test Example', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  it('should navigate to the details screen', async () => {
    await element(by.text('Go to details')).tap();

 await expect(element(by.text('Details Screen'))).toBeVisible();
  });
});
Enter fullscreen mode Exit fullscreen mode

In this test, we verify the navigation by simulating user interaction. E2E tests are crucial to ensure that the app works as expected in a realistic environment, mimicking the final user's interactions.

Conclusion

Applying unit tests, component tests, integration tests, and end-to-end (E2E) tests in React Native app development is essential to ensure the quality and reliability of your product.

  • Unit Tests verify the functionality of small parts of the code.
  • Component Tests ensure that individual components function as expected.
  • Integration Tests check the interaction between different parts of the system.
  • End-to-End (E2E) Tests simulate user behavior, testing the application as a whole.

Implementing a robust testing strategy not only improves code quality but also increases confidence in delivering new features and maintaining the app in the long term. Tools like Jest, Testing Library, and Detox are essential for any React Native developer aiming to deliver a high-quality product.

I hope this guide has been helpful and that you feel more confident in applying tests to your React Native project. Code quality is crucial, and testing is a fundamental step in achieving excellence in software development.

Top comments (0)