Executando verificação de segurança...
1

Testando apps React Native com Detox: do zero ao CI em 2024

Na semana passada, o time do Detox publicou a versão 20.16.0 que traz melhorias de performance para Android e suporte oficial ao React Native 0.73. Embora não seja um lançamento bombástico, a atualização mostra como o framework continua evoluindo silenciosamente e se consolidando como a opção mais confiável para testes end-to-end em projetos React Native. Vamos aproveitar esse momento para entender como implementar testes automatizados que realmente funcionem no seu dia a dia.

Por que testes end-to-end importam (e por que ignoramos eles)

Todo desenvolvedor já ouviu que deve testar seu código, mas na prática criar testes para apps mobile costuma ser uma dor de cabeça. Jest e React Native Testing Library cobrem componentes e lógica, mas não garantem que o usuário consegue realmente fazer login após três toques rápidos no botão enquanto o teclado ainda está aberto. É aí que entram os testes end-to-end (E2E).

A realidade é que muitos times evitam implementar E2E porque parece complexo: precisa configurar emuladores, lidar com timing flaky, esperar builds lentos... Mas o custo de não ter esses testes aparece quando recebemos reviews negativas na loja ou quando um bug crítico chega em produção porque "funcionava no meu celular".

Configurando o Detox em 15 minutos

Vamos criar uma configuração que funcione tanto no iOS quanto no Android, focando no que realmente importa para seu workflow diário.

Primeiro, instale as dependências:

npm install --save-dev detox
npx detox init -r jest

O comando detox init cria a estrutura básica. Agora vamos editar o .detoxrc.js para algo mais realista:

/** @type {Detox.DetoxConfig} */
module.exports = {
  testRunner: {
    args: {
      '$0': 'jest',
      config: 'e2e/jest.config.js'
    },
    jest: {
      setupTimeout: 120000
    }
  },
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/SeuApp.app',
      build: 'xcodebuild -workspace ios/SeuApp.xcworkspace -scheme SeuApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build'
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug'
    }
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 15'
      }
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'Pixel_3_API_30'
      }
    }
  },
  configurations: {
    'ios.sim.debug': {
      device: 'simulator',
      app: 'ios.debug'
    },
    'android.emu.debug': {
      device: 'emulator',
      app: 'android.debug'
    }
  }
};

Escrevendo testes que refletem uso real

Vamos criar um teste para o fluxo de login, mas focando nos comportamentos que realmente acontecem no mundo real:

describe('Fluxo de Login', () => {
  beforeAll(async () => {
    await device.launchApp({
      newInstance: true,
      permissions: { notifications: 'YES', camera: 'YES' }
    });
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('deve fazer login com credenciais válidas', async () => {
    // Espera a tela carregar completamente
    await waitFor(element(by.id('login-screen'))).toBeVisible().withTimeout(5000);
    
    // Preenche os campos com delay realista entre keystrokes
    await element(by.id('input-email')).typeText('[email protected]');
    await element(by.id('input-senha')).typeText('senha123');
    
    // Dismiss do teclado antes de tocar no botão
    await element(by.id('login-screen')).tap();
    
    // Verifica se o botão está enabled antes de tocar
    await expect(element(by.id('botao-entrar'))).toBeVisible();
    await element(by.id('botao-entrar')).tap();
    
    // Espera a navegação acontecer
    await waitFor(element(by.id('home-screen'))).toBeVisible().withTimeout(10000);
    
    // Valida que chegou na home
    await expect(element(by.text('Bem-vindo de volta!'))).toBeVisible();
  });

  it('deve mostrar erro com credenciais inválidas', async () => {
    await element(by.id('input-email')).typeText('[email protected]');
    await element(by.id('input-senha')).typeText('senhaerrada');
    await element(by.id('login-screen')).tap();
    await element(by.id('botao-entrar')).tap();
    
    await expect(element(by.text('Email ou senha incorretos'))).toBeVisible();
  });
});

Trucos para testes mais estáveis

O maior problema com testes E2E é a flakiness - testes que passam às vezes e falham outras. Aqui vão estratégias que funcionam no dia a dia:

1. Use testIDs semânticos

// ❌ Ruim: genérico
<Text testID="text-1">Conteúdo</Text>

// ✅ Bom: específico e semântico
<Text testID="tela-carrinho-texto-valor-total">R$ 99,90</Text>

2. Sincronize com o estado do app

// Sempre espere elementos desaparecerem também
await waitFor(element(by.id('loading-spinner'))).toBeNotVisible().withTimeout(5000);

3. Mock dados realistas

Crie uma função para injetar dados no AsyncStorage antes de cada teste:

async function setupUsuarioLogado() {
  await device.launchApp({
    newInstance: true,
    launchArgs: {
      detoxURLBlacklistRegex: '.*firestore\\.googleapis\\.com.*',
    }
  });
  
  // Injeta o token de autenticação
  await device.execute(async () => {
    const AsyncStorage = require('@react-native-async-storage/async-storage').default;
    await AsyncStorage.setItem('authToken', 'fake-jwt-token');
  });
}

Integrando com seu CI (GitHub Actions)

O segredo é rodar em paralelo e cachear builds. Aqui está uma configuração que reduz o tempo de build em 70%:

name: E2E Tests
on: [push, pull_request]

jobs:
  e2e-ios:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      
      - name: Cache iOS build
        uses: actions/cache@v3
        with:
          path: ios/build
          key: ${{ runner.os }}-ios-build-${{ hashFiles('ios/Podfile.lock') }}
      
      - name: Setup
        run: |
          npm ci
          cd ios && pod install
        
      - name: Run iOS tests
        run: npm run e2e:ios -- --cleanup --headless --take-screenshots failing

  e2e-android:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Android SDK
        uses: android-actions/setup-android@v2
      
      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
      
      - name: Cache Android build
        uses: actions/cache@v3
        with:
          path: |
            android/app/build
            ~/.gradle/caches
          key: ${{ runner.os }}-android-build-${{ hashFiles('android/**/*.gradle*') }}
      
      - name: Run Android tests
        run: npm run e2e:android -- --cleanup --headless --take-screenshots failing

Medindo o impacto real

Depois de implementar Detox em 3 projetos diferentes, os números mostram:

  • **70% redu
Carregando publicação patrocinada...