0

From Novice to Pro: How to test your react native app - Level 5

Learn how to implement test cases for different functional components in react native using jest, maestro and msw along with inline env variables in react native

article cover image

TurjoyAugust 5, 2025

Let's start with the app we have built so far.

We understood OOPs application in popular libraries like Redux Toolkit.

Atomic design principles was followed to make reusable components.

For global state management we have used redux-toolkit.

This will be the starting point our journey: redux-with-api-integration, code.

Go over the explanations and try make something like this final code.


Goals

Our goal in here is to learn different types of tests in react native.

It will be a basic but diverse exposure.

We will be digging into:

  1. Types of testing
  2. UI component testing (unit and snapshot)
  3. Redux testing (Integration testing)
  4. Mocking API servers
  5. Using inline environment variables
  6. End-to-end testing

Types of Tests in React Native

In React Native or any frontend we have four basic types of tests:

Unit Tests

React Native unit tests are small tests that check individual parts of your React Native code to make sure they work correctly.

Integration Tests

React Native integration tests are tests that check how different parts of your React Native app work together to ensure they function correctly as a whole.

Snapshot Tests

React Native snapshot tests capture the current appearance of a component and compare it to a previously saved "snapshot" to ensure no unintended changes occur.

End-to-End Test

End-to-end tests in React Native verify that your entire application works as expected from start to finish, simulating real user interactions across multiple components and screens.


Component Testing

Component testing is basically testing smallest reusable components.

It involves performing unit and snapshot tests

Let's take a simple atomic component.

import { ReactNode } from "react";
import { StyleSheet, View } from "react-native";
import Colors from "../../../constants/Colors";

const BasicContainer = ({ children }: { children: ReactNode }) => {
  return <View style={styles.container}>{children}</View>;
};

export default BasicContainer;

const styles = StyleSheet.create({
  container: {
    justifyContent: "center",
    alignItems: "center",
    height: "100%",
    display: "flex",
    backgroundColor: Colors.bgColor,
  },
});

To test this component we will implement a unit test and a snapshot test.

import React from "react";
import { render } from "@testing-library/react-native";
import BasicContainer from "./container";
import { Text } from "react-native";
import { it } from "@jest/globals";

const MockedChild = () => <Text>Test Children</Text>;
describe("BasicContainer component", () => {
  it("renders children correctly", () => {
    const { getByText } = render(
      <BasicContainer>
        <MockedChild />
      </BasicContainer>
    );
    expect(getByText("Test Children")).toBeTruthy();
  });

  it("matches snapshot", () => {
    const { toJSON } = render(
      <BasicContainer>
        <MockedChild />
      </BasicContainer>
    );
    expect(toJSON()).toMatchSnapshot();
  });
});

MSW

MSW (Mock Service Worker) is a API mocking library.

It lets us test API-dependent components without calling the real API.

Before using MSW, we set it up by defining handlers.

These handlers specify how the mock server should respond to incoming requests.

//mocks/handlers.ts

import {http, HttpResponse} from 'msw';
import mockedApiResponse from './mockedApiResponse.json';
import BaseURL from '../constants/baseUrl';
import {server} from './server';

const allPosts = new Map();

afterEach(() => server.resetHandlers());

export const handlers = [...];

Of course a mocked api response:

//mocks/mockedAPIResponse.js

const mockedAPIResponse = {
  "-NsjUY5FNijKFGUUzC7Q": {
    description: "todo 1",
    pending: true,
  },
  "-NsjUZ2nhml69EBr-K6c": {
    description: "todo 2",
    pending: false,
  },
};

export default mockedAPIResponse;

Finally we define the server:

import { setupServer } from "msw/native";
import { handlers } from "./handlers";

export const server = setupServer(...handlers);

The 'server' will be imported to mock apis while testing.


Jest SpyOn

Jest offers a 'spyOn' method, creating a mock function akin to jest.fn().

A mock function serves to avoid utilizing a real function in code.

'spyOn' monitors calls to object methods.

It then furnishes a mocked response without actually executing the function.

This allows components requiring function output to function with a dummy output during testing when invoking the function.


Redux Testing

We're familiar with unit testing.

Now, let's delve into integration testing.

We'll ensure actions and reducers properly update the store.

Initially, we dispatch the action.

Subsequently, we verify if the store has updated accordingly.

We'll test for three actions: fetchTodos, addTodo, and deleteTodo.

MSW mocks API responses for fetchTodos and deleteTodo, while Jest's spyOn mocks the API response for addTodo.

MSW is used to mock API responses in fetchTodos and deleteTodo.

Jest SpyOn is used to mock API response for addTodo.

// store/__tests__/todo.test.tsx

import { addTodo, deleteTodo, fetchTodos } from "../actions/todo";
import { Services } from "../helpers";

import { server } from "../../mocks/server";
import { handlers } from "../../mocks/handlers";
import store from "..";

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("redux tests", () => {
  afterEach(() => {
    jest.restoreAllMocks();
  });

  const mockId = "123";

  it("should fetch data fulfilled", async () => {
    server.use(handlers[1]);

    await store.dispatch(fetchTodos());

    expect(store.getState().todo).toEqual({
      data: [
        {
          description: "todo 1",
          id: "-NsjUY5FNijKFGUUzC7Q",
          pending: true,
        },
        {
          description: "todo 2",
          id: "-NsjUZ2nhml69EBr-K6c",
          pending: false,
        },
      ],
      error: undefined,
      status: "success",
    });
  });

  it("should post data fulfilled", async () => {
    jest
      .spyOn(Services.prototype, "POST")
      .mockResolvedValueOnce({ name: mockId });

    await store.dispatch(addTodo("mock arg"));

    expect(store.getState().todo).toEqual({
      data: [
        {
          description: "todo 1",
          id: "-NsjUY5FNijKFGUUzC7Q",
          pending: true,
        },
        {
          description: "todo 2",
          id: "-NsjUZ2nhml69EBr-K6c",
          pending: false,
        },
        {
          description: "mock arg",
          id: mockId,
          pending: true,
        },
      ],
      error: undefined,
      status: "success",
    });
  });

  it("should delete data fulfilled", async () => {
    server.use(handlers[0]);

    await store.dispatch(deleteTodo(mockId));

    expect(store.getState().todo).toEqual({
      data: [
        {
          description: "todo 1",
          id: "-NsjUY5FNijKFGUUzC7Q",
          pending: true,
        },
        {
          description: "todo 2",
          id: "-NsjUZ2nhml69EBr-K6c",
          pending: false,
        },
      ],
      error: undefined,
      status: "success",
    });
  });
});

The MSW handlers defined for them are:

//mocks/handlers

import { http, HttpResponse } from "msw";
import mockedApiResponse from "./mockedApiResponse.json";
import BaseURL from "../constants/baseUrl";
import { server } from "./server";
import store from "../store";

afterEach(() => server.resetHandlers());

export const handlers = [
  http.delete(
    `${BaseURL}/todos/:id.json`,
    async ({ request, params, cookies }) => {
      const { id } = params;
      const deletedPost = store
        .getState()
        .todo.data.find((ele) => ele.id === id);

      // Respond with a "404 Not Found" response if the given
      // post ID does not exist.
      if (!deletedPost) {
        return new HttpResponse(null, { status: 404 });
      }

      // Respond with a "200 OK" response and the deleted post.
      return HttpResponse.json(deletedPost);
    }
  ),
  http.get(`${BaseURL}/todos.json`, async ({ request, params, cookies }) => {
    return HttpResponse.json(mockedApiResponse);
  }),
];

Methods used for Jest.spyOn :

//store/helpers.tsx

import { Task } from "../../types";
import BaseURL from "../constants/baseUrl";

export class Services {
  async POST(todo: Task) {
    const response = await fetch(`${BaseURL}/todos.json`, {
      method: "POST",
      body: JSON.stringify({ ...todo }),
    });
    if (!response.ok) {
      return Promise.reject(`HTTP error! Status: ${response.status}`);
    }
    const resData = await response.json();

    return resData;
  }

  async GET() {
    const response = await fetch(`${BaseURL}/todos.json`);

    if (!response.ok) {
      return Promise.reject(`HTTP error! Status: ${response.status}`);
    }
    const resData = await response.json();

    return resData;
  }
}

The MSW catches API calls using the top three commands:

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

E2E using Maestro

Maestro is a End to End testing tool for React Native.

To use Maestro we ensure if the components we want to use are accessible to Maestro.

Maestro detects components from their text parts.

But in our case we have an icon button.

To let Maestro be aware of the icon component, we add text in the "accessiblityLabel" of the component.

const CustomFAB = ({
  onPress,
}: {
  onPress: (event: GestureResponderEvent) => void;
}) => {
  return (
    <View style={styles.FABContainer} accessibilityLabel={"FabButton"}>
      <FAB
        icon="plus"
        style={{
          backgroundColor: Colors.fabColor,
          borderRadius: 60,
        }}
        onPress={onPress}
      />
    </View>
  );
};

So first we define a setup.js file with texts from components to detect.

This is optional, but good practice.

output.screens = {
  TodoList: {
    fabButton: "FabButton",
  },
  AddTodo: {
    inputPlace: "Todo",
    inputText: "Todo it!",
  },
};

Then we provide the steps the E2E test should perform.

The steps are pretty self-explanatory in the yaml code below.

//e2e/flow.yaml

appId: com.todoapp
---
- launchApp
- runScript: ./setup.js
- tapOn: ${output.screens.TodoList.fabButton}
- tapOn: ${output.screens.AddTodo.inputPlace}
- inputText: ${output.screens.AddTodo.inputText}
- tapOn: ${output.screens.TodoList.fabButton}
- assertVisible: ${output.screens.AddTodo.inputText}

Refer to the video at the top to see this in action!


MSW and ENV variables with Maestro

While testing we don't want to use production APIs.

We would get our APIs for production, development and testing separately.

Also when E2E testing, using mock APIs with MSW is the best option.

ENV setup and MSW preload:

  1. Install react-native-config:

    Follow the instructions given in the repo( react-native-config).

    There is a alternative react-native-dot-env.

    But it doesn't support inline environment variables well enough.

  2. Env files setup:

    You just create files like

    .env.dev .env.test .env.prod

    You fill these files with key, values like

    DB_URL=https://****.firebasedatabase.app
    APP_ENV=dev
    

    and then just use it:

    import Config from "react-native-config";
    
    Config.DB_URL;
    Config.APP_ENV;
    

    If you want to use different environments, you basically set ENVFILE variable like this:

    ENVFILE=.env.test react-native run-android
    

    or for assembling app for production (android in my case):

    cd android && ENVFILE=.env.prod ./gradlew assembleRelease
    
  3. Set up MSW for E2E mode:

    // app.tsx
    import Config from "react-native-config";
    
    async function enableMocking(): Promise<void> {
      if (Config.APP_ENV !== "test") {
        return;
      }
    
      await import("./msw.polyfills");
      const { server } = await import("./src/__mock__/server");
      server.listen();
    }
    
    function App() {
      const [loading, setLoading] = React.useState(true);
    
      React.useEffect(() => {
        enableMocking().then(() => setLoading(false));
      }, []);
    
      return (
        !loading && (
          <PaperProvider>
            <Provider store={store}>
              <AppNavigator />
            </Provider>
          </PaperProvider>
        )
      );
    }
    

    and add ts configuration to module ES2022

    //tsconfig.json
    {
      "extends": "@react-native/typescript-config/tsconfig.json",
      "compilerOptions": {
        ...
        "module": "ES2022"
      }
    }
    

    This differs from to MSW-RN.

    But works as expected

    It loads data without MSW first, then recognises MSW.

    To run maestro, open two terminal in root of the project.

    In one execute:

    yarn start:test
    

    In another run:

    yarn test:e2e
    

    Refer to package.json file for breakdown of the script commands.


Next Steps:

  1. Create an integration test of components using Redux.
  2. Try React Navigation Testing
  3. Create E2E test for marking checkbox (Hint use variable accessibility text to detect checkbox)

Resources

Liked what you read?

Join my email list to get more...