Conceptos básicos de pruebas mobile
10 ejemplos para empezar con Pruebas Mobile - 7 básicos e intermedios.
Busca en todas las páginas de la documentación
10 ejemplos para empezar con Pruebas Mobile - 7 básicos e intermedios.
Expo SDK 57 incluye integración con Jest a través de jest-expo. Agrega React Native Testing Library para pruebas de componentes:
npx create-expo-app@latest MyTestApp --template blank-typescript
cd MyTestApp
npx expo install jest-expo @testing-library/react-native @types/jest --devConfigura el preset en package.json y agrega un archivo de configuración para los matchers de RNTL:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
},
"jest": {
"preset": "jest-expo",
"setupFilesAfterEnv": ["<rootDir>/jest.setup.ts"]
}
}// jest.setup.ts
import "@testing-library/react-native/extend-expect";Ejecuta la suite localmente antes de cada PR:
npm testHerramientas: Estos ejemplos están dirigidos a Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, y React 19.2.3. Los ejemplos E2E asumen una compilación dev o release - Expo Go por sí solo no es suficiente para Detox ni para la mayoría de flujos de producción de Maestro.
Las aplicaciones móviles necesitan una estrategia en capas: las pruebas rápidas se ejecutan en cada guardado; las pruebas lentas se ejecutan antes del lanzamiento.
┌─────────────┐
│ Flujos E2E │ pocos - Maestro / Detox en compilaciones reales
├─────────────┤
│ Integración │ algunos - pantallas + navegación + APIs simuladas
├─────────────┤
│ Componente │ muchos - RNTL, comportamiento visible para el usuario
├─────────────┤
│ Unidad │ la mayoría - funciones puras, reductores, lógica de hooks
└─────────────┘| Capa | Se ejecuta en | Herramientas típicas | Cuando falla |
|---|---|---|---|
| Unidad | Node (ms) | Jest | Matemáticas mal, transiciones de estado mal |
| Componente | Node (ms) | Jest + RNTL | Etiqueta incorrecta, botón faltante |
| Integración | Node (s) | Jest + RNTL + mocks | El flujo de pantalla se rompe entre archivos |
| E2E | Simulador/dispositivo (min) | Maestro, Detox | Navegación real, puentes nativos, sincronización |
expo-secure-store, fetch, y sensores en Jest; pruébalos en E2ERelacionado: Testing Best Practices - qué ejecutar antes de cada envío a la tienda
Las pruebas unitarias son la base de la pirámide: sin árbol de React, sin módulos nativos, retroalimentación de sub-milisegundos.
// src/lib/formatPrice.ts
export function formatPrice(cents: number, currency = "USD"): string {
if (!Number.isFinite(cents) || cents < 0) {
throw new RangeError("cents must be a non-negative finite number");
}
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
}).format(cents / 100);
}// src/lib/formatPrice.test.ts
import { formatPrice } from "./formatPrice";
describe("formatPrice", () => {
it("formats whole dollars", () => {
expect(formatPrice(1200)).toBe("$12.00");
});
it("rejects negative values", () => {
expect(() => formatPrice(-1)).toThrow(RangeError);
});
});*.test.ts o bajo __tests__/ - ambas funcionan; elige una convención por reporender() aquí - si necesitas un árbol de componentes, eso es una prueba de componente o integraciónRelacionado: Jest Setup for Expo - preset, transforms, y disciplina de snapshots
Las pruebas de componentes afirman lo que el usuario ve - texto, roles, y resultados de presiones - no nombres de variables de estado interno.
// src/components/PrimaryButton.tsx
import { Pressable, Text, StyleSheet } from "react-native";
type Props = {
label: string;
onPress: () => void;
};
export function PrimaryButton({ label, onPress }: Props) {
return (
<Pressable
accessibilityRole="button"
accessibilityLabel={label}
onPress={onPress}
style={styles.button}
>
<Text style={styles.label}>{label}</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
button: { backgroundColor: "#2563eb", padding: 12, borderRadius: 8 },
label: { color: "#fff", textAlign: "center", fontWeight: "600" },
});// src/components/PrimaryButton.test.tsx
import { render, screen, userEvent } from "@testing-library/react-native";
import { PrimaryButton } from "./PrimaryButton";
describe("PrimaryButton", () => {
it("calls onPress when tapped", async () => {
const user = userEvent.setup();
const onPress = jest.fn();
render(<PrimaryButton label="Continue" onPress={onPress} />);
await user.press(screen.getByRole("button", { name: "Continue" }));
expect(onPress).toHaveBeenCalledTimes(1);
});
});userEvent.press simula el ciclo de vida completo de presión - prefierelo sobre fireEvent en bruto para un comportamiento más cercano al usuariogetByRole + name cuando sea posible - obliga etiquetas accesibles que reutilizarás en E2Erender todavía se ejecuta en Node; las vistas nativas están simuladas - por eso la capa es rápidaRelacionado: React Native Testing Library - prioridad de consulta, utilidades async, y antipatrones
La prioridad de consulta de RNTL refleja accesibilidad: etiqueta y rol vencen testID, y testID vence selectores frágiles similares a CSS (que no existen en RN de todos modos).
import { View, Text, TextInput, StyleSheet } from "react-native";
export function EmailField() {
return (
<View>
<Text nativeID="email-label">Email</Text>
<TextInput
accessibilityLabel="Email"
accessibilityLabelledBy="email-label"
placeholder="you@example.com"
style={styles.input}
/>
</View>
);
}
const styles = StyleSheet.create({
input: { borderWidth: 1, borderColor: "#d1d5db", padding: 10, borderRadius: 8 },
});import { render, screen } from "@testing-library/react-native";
import { EmailField } from "./EmailField";
it("exposes the email field by accessible name", () => {
render(<EmailField />);
expect(screen.getByLabelText("Email")).toBeTruthy();
});getByLabelText vincula pruebas con las mismas cadenas que VoiceOver y TalkBack leen en voz altatestID solo cuando rol y etiqueta no pueden desambiguar controles duplicadosUNSAFE_getByType) - los refactores rompen esas pruebas instantáneamenteRelacionado: React Native Testing Library -
within,findBy*async, y salida de debug
Jest se ejecuta en Node - los puentes nativos no están disponibles a menos que estén simulados. Simula el módulo, no los internos de tu componente.
// src/hooks/useHapticTap.ts
import * as Haptics from "expo-haptics";
export async function useHapticTap() {
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}// src/hooks/useHapticTap.test.ts
jest.mock("expo-haptics", () => ({
impactAsync: jest.fn(),
ImpactFeedbackStyle: { Light: "light" },
}));
import * as Haptics from "expo-haptics";
import { useHapticTap } from "./useHapticTap";
it("fires light impact on tap", async () => {
await useHapticTap();
expect(Haptics.impactAsync).toHaveBeenCalledWith("light");
});jest.mock antes de imports del módulo bajo prueba (Jest eleva mocks, pero mantén el patrón consistente)jest-expo simula automáticamente muchos módulos de Expo; anula cuando necesites valores de retorno determinísticosRelacionado: Mocking Native Modules - mocks manuales,
__mocks__, y trampas de falsa confianza
Las pruebas de integración renderizen una pantalla (o porción de navegador), interactúan entre componentes hijo, y afirman el resultado combinado.
// src/screens/SignInScreen.tsx
import { useState } from "react";
import { View, Text, TextInput, Pressable, StyleSheet } from "react-native";
export function SignInScreen({ onSubmit }: { onSubmit: (email: string) => void }) {
const [email, setEmail] = useState("");
const canSubmit = email.includes("@");
return (
<View style={styles.screen}>
<Text accessibilityRole="header">Sign in</Text>
<TextInput
accessibilityLabel="Email"
value={email}
onChangeText={setEmail}
autoCapitalize="none"
style={styles.input}
/>
<Pressable
accessibilityRole="button"
accessibilityLabel="Continue"
disabled={!canSubmit}
onPress={() => onSubmit(email)}
style={[styles.button, !canSubmit && styles.disabled]}
>
<Text style={styles.buttonText}>Continue</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { padding: 24, gap: 12 },
input: { borderWidth: 1, borderColor: "#d1d5db", padding: 10, borderRadius: 8 },
button: { backgroundColor: "#2563eb", padding: 12, borderRadius: 8 },
disabled: { opacity: 0.4 },
buttonText: { color: "#fff", textAlign: "center", fontWeight: "600" },
});// src/screens/SignInScreen.test.tsx
import { render, screen, userEvent } from "@testing-library/react-native";
import { SignInScreen } from "./SignInScreen";
it("submits a valid email", async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<SignInScreen onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "user@example.com");
await user.press(screen.getByRole("button", { name: "Continue" }));
expect(onSubmit).toHaveBeenCalledWith("user@example.com");
});fetch, expo-secure-store) para que la prueba permanezca determinísticaRelacionado: Jest Setup for Expo - navegación simulada y fetch global
Los snapshots detectan cambios de árbol visual no intencionales, pero el uso excesivo crea diffs ruidosos que los equipos sellan con el pulgar.
import { render } from "@testing-library/react-native";
import { Text, View } from "react-native";
function Badge({ count }: { count: number }) {
return (
<View>
<Text>{count} unread</Text>
</View>
);
}
it("matches stable badge markup", () => {
const { toJSON } = render(<Badge count={3} />);
expect(toJSON()).toMatchSnapshot();
});getByRoleRelacionado: Jest Setup for Expo - serializadores de snapshot y flujo de actualización
waitForLa UI respaldada por red necesita consultas async - renderiza carga primero, luego afirma contenido asentado.
import { useEffect, useState } from "react";
import { Text, View, ActivityIndicator } from "react-native";
type Profile = { name: string };
export function ProfileCard({ userId }: { userId: string }) {
const [profile, setProfile] = useState<Profile | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
fetch(`https://api.example.com/users/${userId}`)
.then((res) => res.json())
.then((data: Profile) => {
if (!cancelled) setProfile(data);
})
.catch(() => {
if (!cancelled) setError("Could not load profile");
});
return () => {
cancelled = true;
};
}, [userId]);
if (error) return <Text>{error}</Text>;
if (!profile) return <ActivityIndicator accessibilityLabel="Loading profile" />;
return (
<View>
<Text accessibilityRole="header">{profile.name}</Text>
</View>
);
}import { render, screen, waitFor } from "@testing-library/react-native";
import { ProfileCard } from "./ProfileCard";
beforeEach(() => {
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({ name: "Riley" }),
}) as jest.Mock;
});
it("shows the profile name after fetch resolves", async () => {
render(<ProfileCard userId="42" />);
expect(screen.getByLabelText("Loading profile")).toBeTruthy();
await waitFor(() => {
expect(screen.getByRole("header", { name: "Riley" })).toBeTruthy();
});
});findBy* como azúcar para waitFor + getBy* cuando solo necesitas presenciafetch (o MSW) en Jest - nunca hagas llamadas a APIs reales en pruebas unitarias o de integraciónmockRejectedValue - las redes móviles fallan constantementeRelacionado: Mocking Native Modules - stub
fetchy módulos de red de Expo
Las pruebas end-to-end se sientan en la cima de la pirámide - pocos flujos, binario real, gestos reales. Elige el corredor por pipeline de compilación y tolerancia de inestabilidad.
# .maestro/flows/sign-in.yaml
appId: com.example.mytestapp
---
- launchApp
- tapOn: "Sign in"
- inputText: "user@example.com"
- tapOn: "Continue"
- assertVisible: "Welcome"// e2e/signIn.e2e.ts - Detox (requires dev client / release build)
describe("Sign in", () => {
beforeAll(async () => {
await device.launchApp();
});
it("shows welcome after valid email", async () => {
await element(by.text("Sign in")).tap();
await element(by.label("Email")).typeText("user@example.com");
await element(by.text("Continue")).tap();
await expect(element(by.text("Welcome"))).toBeVisible();
});
});| Herramienta | Mejor para | Intercambio |
|---|---|---|
| Maestro | Flujos de humo YAML, CI sin harness de prueba nativa pesada | Menos control gris-caja sobre temporizadores nativos async |
| Detox | Sincronización nativa profunda, biometría, pilas de navegación complejas | Mayor costo de configuración, toolchain de Xcode/Android requerido |
expo run:ios en CI primeroRelacionado: Maestro E2E - flujos YAML y controles de pruebas inestables | Detox E2E - sincronización gris-caja
package.json en capas y guardias de contratoVincula la pirámide con comandos que los desarrolladores realmente ejecutan - y protege formas de API para que las pruebas de integración no mientan.
{
"scripts": {
"test": "jest --ci",
"test:unit": "jest --testPathPattern=\\.test\\.ts$",
"test:integration": "jest --testPathPattern=screens/",
"test:e2e:maestro": "maestro test .maestro/flows",
"test:contract": "jest --testPathPattern=contract"
}
}// src/api/contracts/userSchema.ts
import { z } from "zod";
export const userSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
displayName: z.string().min(1),
});
export type User = z.infer<typeof userSchema>;// src/api/contracts/userSchema.contract.test.ts
import { userSchema } from "./userSchema";
const fixture = {
id: "550e8400-e29b-41d4-a716-446655440000",
email: "user@example.com",
displayName: "Riley",
};
it("mobile client accepts the user payload the server documents", () => {
expect(() => userSchema.parse(fixture)).not.toThrow();
});test:unit vs test:integration deja que CI paralelice trabajos rápidos y lentos - misma pirámide, diferentes corredoresmain, y revisita Testing Best PracticesRelacionado: Contract Tests for APIs - Pact y estrategias de schema | Visual Regression & Storybook - catálogos de componentes en dispositivo
Versiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026