Jest
react native
mobile development
testing
monitoring
quality-assurance
When building a mobile application, the real challenge is not writing code, it's making sure it works well today, tomorrow, and after future changes. Without testing or monitoring, every code change is a risk, bugs reach production unnoticed, and you rely on the end user to find out if something broke.
On the other hand, with a proper testing and monitoring strategy, we can detect errors before they reach the user, reduce maintenance and support costs, and make informed decisions with real data about the app's behavior.
Testing = prevention
Monitoring = detection and analysis
And both are complementary.
Testing means verifying that the code does what it should, consistently and without unexpected side effects. When there are no automated tests, the only way to validate changes is to open the app, navigate, tap buttons, and hope nothing breaks. This is costly, slow, and unreliable.
With automated testing:
| Test type | Objective | Tool | Example use case |
|---|---|---|---|
| Unit | Validate pure logic | Jest | check calculations, validations, data formatting |
| Component (UI) | Validate screen behaviors | React Native Testing Library | check that a button increments a counter |
| Integration | Validate interaction between modules | Jest + mocks | check that a component correctly calls a service |
Key rule: tests should validate behaviors, not implementations.
First, something simple: a function that calculates the total of a purchase.
1// utils/calculateTotal.ts 2export const calculateTotal = (price: number, qty: number) => price * qty;
Let's create a test to validate that behavior:
1// __tests__/calculateTotal.test.ts 2import { calculateTotal } from "../utils/calculateTotal"; 3 4test("correctly calculates the total", () => { 5 expect(calculateTotal(10, 3)).toBe(30); 6});
With this test, you ensure that if someone changes that function or its behavior, you'll be alerted immediately.
Here we don't test how the component is built, but what it does when the user interacts.
1// components/Counter.tsx 2import React, { useState } from "react"; 3import { Button, Text } from "react-native"; 4 5export const Counter = () => { 6 const [count, setCount] = useState(0); 7 8 return ( 9 <> 10 <Text testID="count-label">{count}</Text> 11 <Button title="Increment" onPress={() => setCount(count + 1)} /> 12 </> 13 ); 14};
Test:
1// __tests__/Counter.test.tsx 2import React from "react"; 3import { render, fireEvent } from "@testing-library/react-native"; 4import { Counter } from "../components/Counter"; 5 6test("increments the counter when pressed", () => { 7 const { getByText, getByTestId } = render(<Counter />); 8 9 fireEvent.press(getByText("Increment")); 10 11 expect(getByTestId("count-label").props.children).toBe(1); 12});
What we're validating:
Although tests prevent errors, no system is free from failures in production. Therefore, you need to know which screen was being used when it failed, what action the user performed, what data was involved; that's where structured logging comes in.
1console.log("User logged in", user);
Problems:
1console.log(JSON.stringify({ 2 event: "USER_LOGIN", 3 userId: user.id, 4 timestamp: Date.now(), 5}));
Advantages:
1// utils/logger.ts 2export const logEvent = (event: string, payload = {}) => { 3 console.log(JSON.stringify({ event, payload, timestamp: Date.now() })); 4};
Usage:
1logEvent("API_REQUEST", { endpoint: "/products" });
This pattern centralizes logging responsibility.
One of the most frequent questions when starting to test in React Native is: “Should I test all my code?” The answer is no; testing doesn't mean replicating every component or line of code, it means protecting the parts of the system that, if they fail, directly affect the user or the business.
Here are four essential best practices specifically for React Native CLI projects.
Test logic that could break the app: In React Native, most important decisions aren't made in components, but in:
- helper functions (utils/)
- business hooks (hooks/)
- services and controllers (services/)
This logic is usually independent of the UI, making it easier to test without rendering a screen.
1// utils/formatCurrency.ts 2export const formatCurrency = (amount: number) => `$${amount.toFixed(2)}`;
Unit test:
1test("formats a number as currency", () => { 2 expect(formatCurrency(10)).toBe("$10.00"); 3});
Why does it matter? If someone modifies this function in the future (for example, to support another currency), a test will prevent them from introducing an error by accident.
Typical cases in a mobile app:
| Critical case | Risk if it fails | What to test |
|---|---|---|
| Login | User can't use the app | Form validation, backend calls |
| Purchase/payment process | Economic losses | Calculations, totals, payment methods |
| Forms with rules | Broken user flow | Error messages and button states |
This gives you 80% risk coverage with 20% effort.
__DEV__) you see the console, but in production the app runs on a device you can't access. That's why logs must be structured, consistent, and contextual.1console.log("Login error");
1logEvent("LOGIN_FAILED", { 2 email, 3 reason: error.message, 4});
This format allows you to:
Minimal implementation in React Native CLI:
1// utils/logger.ts 2export const logEvent = (event: string, payload = {}) => { 3 const log = { event, payload, timestamp: Date.now() }; 4 5 if (__DEV__) { 6 console.log(JSON.stringify(log, null, 2)); 7 } else { 8 // production → send to an external provider 9 // Sentry.captureMessage(JSON.stringify(log)); 10 } 11};
console.log without structureIn React Native CLI, app quality doesn't just depend on how well it's developed, but on how well you can know when something stops working. Testing ensures your app works as it should, and monitoring ensures you'll know when it doesn't. A professional app requires both.