Noções Básicas de Testes Mobile
10 exemplos para você começar com Testes Mobile - 7 básicos e 3 intermediários.
Busque em todas as páginas da documentação
10 exemplos para você começar com Testes Mobile - 7 básicos e 3 intermediários.
O Expo SDK 57 inclui integração com Jest através do jest-expo. Adicione o React Native Testing Library para testes de componentes:
npx create-expo-app@latest MyTestApp --template blank-typescript
cd MyTestApp
npx expo install jest-expo @testing-library/react-native @types/jest --devConfigure o preset no package.json e adicione um arquivo de configuração para os matchers do 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";Execute a suíte localmente antes de cada PR:
npm testFerramentas: Estes exemplos visam o Expo SDK 57 (
expo~57.0.4), React Native 0.86.0 e React 19.2.3. Snippets E2E assumem um build de desenvolvimento ou de lançamento - o Expo Go sozinho não é suficiente para Detox ou a maioria dos fluxos de produção do Maestro.
Aplicativos mobile precisam de uma estratégia em camadas: testes rápidos rodam a cada salvamento; testes lentos rodam antes do lançamento.
┌─────────────┐
│ Fluxos E2E │ poucos - Maestro / Detox em builds reais
├─────────────┤
│ Integração │ alguns - telas + navegação + APIs mockadas
├─────────────┤
│ Componente │ muitos - RNTL, comportamento visível ao usuário
├─────────────┤
│ Unitário │ a maioria - funções puras, reducers, lógica de hooks
└─────────────┘| Camada | Roda em | Ferramentas Típicas | Quando falha |
|---|---|---|---|
| Unitário | Node (ms) | Jest | Matemática incorreta, transições de estado ruins |
| Componente | Node (ms) | Jest + RNTL | Rótulo incorreto, botão faltando |
| Integração | Node (s) | Jest + RNTL + mocks | Fluxo da tela quebra entre arquivos |
| E2E | Simulador/dispositivo (min) | Maestro, Detox | Navegação real, pontes nativas, timing |
expo-secure-store, fetch e sensores no Jest; exercite-os em E2ERelacionado: Melhores Práticas de Testes - o que rodar antes de cada submissão para a loja
Testes unitários são a base da pirâmide: sem árvore React, sem módulos nativos, feedback sub-milissegundo.
// 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 ao lado do módulo ou em __tests__/ - ambos funcionam; escolha uma convenção por repositóriorender() aqui - se você precisa de uma árvore de componentes, isso é um teste de componente ou de integraçãoRelacionado: Configuração do Jest para Expo - preset, transforms e disciplina de snapshot
Testes de componente afirmam o que o usuário vê - texto, roles e resultados de toque - não nomes de variáveis de estado internos.
// 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 o ciclo completo de toque - prefira-o em vez de fireEvent bruto para um comportamento mais próximo do usuáriogetByRole + name quando possível - isso impõe rótulos acessíveis que você reutilizará em E2Erender ainda roda no Node; views nativas são simuladas - é por isso que a camada é rápidaRelacionado: React Native Testing Library - prioridade de consulta, utilitários assíncronos e anti-padrões
A prioridade de consulta do RNTL espelha a acessibilidade: rótulo e role precedem testID, e testID precede seletores frágeis semelhantes a CSS (que não existem em RN de qualquer forma).
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 os testes às mesmas strings que VoiceOver e TalkBack leem em voz altatestID apenas quando role e rótulo não puderem desambiguar controles duplicadosUNSAFE_getByType) - refatorações quebram esses testes instantaneamenteRelacionado: React Native Testing Library -
within,findBy*assíncrono e saída de depuração
Jest roda no Node - pontes nativas não estão disponíveis a menos que simuladas. Simule o módulo, não os internos do seu 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 das importações do módulo sob teste (Jest eleva mocks, mas mantenha o padrão consistente)jest-expo simula automaticamente muitos módulos Expo; substitua quando precisar de valores de retorno determinísticosRelacionado: Simulando Módulos Nativos - mocks manuais,
__mocks__e armadilhas de falsa confiança
Testes de integração renderizam uma tela (ou uma fatia do navegador), interagem entre componentes filhos e afirmam o 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 o teste permaneça determinísticoNavigationContainer de teste e alimente o estado inicialRelacionado: Configuração do Jest para Expo - simulando navegação e fetch global
Snapshots capturam desvios visuais não intencionais na árvore, mas o uso excessivo cria diffs barulhentos que as equipes carimbam sem ler.
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: Configuração do Jest para Expo - serializadores de snapshot e fluxo de atualização
waitForUI baseada em rede precisa de consultas assíncronas - renderiza o carregamento primeiro, depois afirma o conteúdo finalizado.
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 açúcar para waitFor + getBy* quando você só precisa de presençafetch (ou MSW) no Jest - nunca acesse APIs reais em testes unitários ou de integraçãomockRejectedValue - redes móveis falham constantementeRelacionado: Simulando Módulos Nativos - stubbing de
fetche módulos de rede Expo
Testes end-to-end ficam no topo da pirâmide - poucos fluxos, binário real, gestos reais. Escolha o runner pela pipeline de build e tolerância a instabilidade.
# .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 (requer cliente de desenvolvimento / build de lançamento)
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();
});
});| Ferramenta | Melhor para | Compromisso |
|---|---|---|
| Maestro | Fluxos de fumaça YAML, CI sem um pesado harness de teste nativo | Menos controle "gray-box" sobre timers nativos assíncronos |
| Detox | Sincronização nativa profunda, biometria, pilhas de navegação complexas | Custo de configuração mais alto, requer toolchain Xcode/Android |
expo run:ios na CI primeiroRelacionado: Maestro E2E - fluxos YAML e controles de testes instáveis | Detox E2E - sincronização "gray-box"
package.json em Camadas e Guardas de ContratoConecte a pirâmide a comandos que os desenvolvedores realmente executam - e proteja as formas da API para que os testes de integração não mintam.
{
"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 permite que a CI paralelize trabalhos rápidos e lentos - mesma pirâmide, runners diferentesmain, e revise Melhores Práticas de TestesRelacionado: Testes de Contrato para APIs - estratégias de Pact e schema | Regressão Visual e Storybook - catálogos de componentes no dispositivo
Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026