React Native testiranje u 2026: Maestro, Detox, Jest i kompletni vodič za E2E testove

Naučite slojeviti pristup testiranju React Native aplikacija u 2026. Jest za unit testove, RNTL za komponente, Maestro i Detox za E2E — s praktičnim primjerima koda i GitHub Actions CI/CD integracijom.

Zašto je testiranje React Native aplikacija poseban izazov

Budimo iskreni: testiranje mobilnih aplikacija nikada nije bilo jednostavno. Ali testiranje React Native aplikacija? To je potpuno druga priča. Imate JavaScript sloj, imate nativni sloj, imate most između njih (ili u 2026. JSI koji ga je zamijenio), imate različite platforme s različitim ponašanjima, imate asinkrone operacije doslovno na svakom koraku. I onda se čudite zašto vam testovi padaju svaki drugi put.

Ako ste ikada proveli cijelo poslijepodne pokušavajući shvatiti zašto vam E2E test prolazi na iOS-u ali pada na Androidu, ili zašto test koji je jučer radio danas javlja timeout error — znate točno o čemu pričam. React Native aplikacije moraju raditi na dva potpuno različita operativna sustava, s različitim renderiranjem, različitim gestama, različitim životnim ciklusom. To jednostavno nije trivijalan problem.

Dobra vijest? U 2026. godini situacija je dramatično bolja nego što je bila. New Architecture je postala standard od React Native 0.82, Fabric i TurboModules su dozreli, a alati za testiranje su konačno uhvatili korak s kompleksnošću ekosustava. Jest je brži nego ikad. React Native Testing Library je zamijenio zastarjeli react-test-renderer i postao de facto standard za testiranje komponenata. A za E2E testove, tu su dva odlična alata — Maestro i Detox — svaki sa svojim pristupom i prednostima.

U ovom vodiču proći ćemo kroz kompletnu strategiju testiranja React Native aplikacija u 2026. Od unit testova do end-to-end scenarija, s primjerima koda koje možete odmah primijeniti. Nema teorije radi teorije — samo alati, konfiguracije i prakse koje stvarno rade u produkciji.

Slojeviti pristup testiranju: od unit testova do E2E

Prije nego uopće otvorimo terminal i počnemo instalirati alate, hajmo razgovarati o strategiji. Najskuplji testovi su oni koji testiraju krivu stvar na krivoj razini — i to sam naučio na teži način.

Piramida testiranja ostaje najkorisniji mentalni model za organizaciju testova u React Native projektu:

        /\
       /  \        E2E (Maestro / Detox)
      /    \       — Kompletni korisnički tokovi
     /------\
    /        \     Komponente (RNTL)
   /          \    — UI komponente + interakcije + stanje
  /------------\
 /              \   Unit (Jest)
/                \  — Čiste funkcije, hookovi, uslužne funkcije
──────────────────

Opće pravilo: otprilike 70% unit testova, 20% testova komponenata i 10% E2E testova. Ovi postoci naravno nisu dogma, ali daju jasnu smjernicu. Najčešća greška koju viđam u timovima je obrnuta piramida — gomila E2E testova, gotovo nikakvi unit testovi — što rezultira sporim, nestabilnim i skupim test suiteom koji nitko ne voli pokretati.

Unit testovi s Jestom

Jest je zadani okvir za testiranje u React Native ekosustavu i tu nema previše debate. Od verzije 30, Jest je dobio značajna poboljšanja: 37% brže izvršavanje, 77% manje korištenje memorije i nativnu podršku za TypeScript konfiguraciju. Za React Native projekt, Jest pokriva sve što je čisti JavaScript — uslužne funkcije, transformacije podataka, custom hookove, store logiku.

Osnovna konfiguracija za React Native projekt:

// jest.config.ts
import type { Config } from 'jest';

const config: Config = {
  preset: 'react-native',
  setupFilesAfterSetup: ['./jest.setup.ts'],
  transformIgnorePatterns: [
    'node_modules/(?!(react-native|@react-native|@react-navigation|expo|@expo)/)',
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
  ],
};

export default config;

Primjer unit testa za jednu jednostavnu uslužnu funkciju:

// utils/formatPrice.ts
export function formatPrice(amount: number, currency: string = 'EUR'): string {
  return new Intl.NumberFormat('hr-HR', {
    style: 'currency',
    currency,
  }).format(amount);
}

// __tests__/formatPrice.test.ts
import { formatPrice } from '../utils/formatPrice';

describe('formatPrice', () => {
  it('formatira cijenu u eurima', () => {
    expect(formatPrice(29.99)).toBe('29,99\u00a0EUR');
  });

  it('formatira cijenu u dolarima', () => {
    expect(formatPrice(100, 'USD')).toBe('100,00\u00a0USD');
  });

  it('ispravno rukuje nulom', () => {
    expect(formatPrice(0)).toBe('0,00\u00a0EUR');
  });

  it('ispravno rukuje negativnim iznosima', () => {
    expect(formatPrice(-15.5)).toContain('15,50');
  });
});

Unit testovi su vaša prva linija obrane. Brzi su (izvršavaju se u milisekundama), pouzdani su (nema flakiness problema), i jeftini su za održavanje. Iskreno, svaka čista funkcija u vašem projektu trebala bi imati unit test — nema izgovora.

Testovi komponenata s React Native Testing Library

E sad, ovdje stvari postaju zanimljivije. React Native Testing Library (RNTL) je zamjena za zastarjeli react-test-renderer, koji više nije kompatibilan s Reactom 19+. RNTL vam omogućuje testiranje komponenata onako kako ih korisnik zapravo vidi — ne testirate implementacijske detalje, nego ponašanje.

Instalacija je trivijalna:

npm install --save-dev @testing-library/react-native

Pogledajmo praktičan primjer. Recimo da imate komponentu za prijavu:

// components/LoginForm.tsx
import React, { useState } from 'react';
import { View, TextInput, Pressable, Text, ActivityIndicator } from 'react-native';

interface LoginFormProps {
  onSubmit: (email: string, password: string) => Promise<void>;
}

export function LoginForm({ onSubmit }: LoginFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async () => {
    if (!email || !password) {
      setError('Sva polja su obavezna');
      return;
    }
    setLoading(true);
    setError('');
    try {
      await onSubmit(email, password);
    } catch (e) {
      setError('Neispravni podaci za prijavu');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View testID="login-form">
      <TextInput
        testID="email-input"
        placeholder="Email"
        value={email}
        onChangeText={setEmail}
        autoCapitalize="none"
        keyboardType="email-address"
      />
      <TextInput
        testID="password-input"
        placeholder="Lozinka"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      {error ? <Text testID="error-text">{error}</Text> : null}
      <Pressable testID="submit-button" onPress={handleSubmit} disabled={loading}>
        {loading ? (
          <ActivityIndicator testID="loading-indicator" />
        ) : (
          <Text>Prijava</Text>
        )}
      </Pressable>
    </View>
  );
}

A evo i testa za tu komponentu:

// __tests__/LoginForm.test.tsx
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { LoginForm } from '../components/LoginForm';

describe('LoginForm', () => {
  const mockOnSubmit = jest.fn();

  beforeEach(() => {
    mockOnSubmit.mockReset();
  });

  it('prikazuje polja za email i lozinku', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    expect(screen.getByTestId('email-input')).toBeTruthy();
    expect(screen.getByTestId('password-input')).toBeTruthy();
    expect(screen.getByTestId('submit-button')).toBeTruthy();
  });

  it('prikazuje grešku kada su polja prazna', () => {
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.press(screen.getByTestId('submit-button'));

    expect(screen.getByTestId('error-text')).toHaveTextContent(
      'Sva polja su obavezna'
    );
    expect(mockOnSubmit).not.toHaveBeenCalled();
  });

  it('poziva onSubmit s ispravnim podacima', async () => {
    mockOnSubmit.mockResolvedValueOnce(undefined);
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByTestId('email-input'), '[email protected]');
    fireEvent.changeText(screen.getByTestId('password-input'), 'sigurna-lozinka');
    fireEvent.press(screen.getByTestId('submit-button'));

    await waitFor(() => {
      expect(mockOnSubmit).toHaveBeenCalledWith('[email protected]', 'sigurna-lozinka');
    });
  });

  it('prikazuje indikator učitavanja tijekom slanja', async () => {
    mockOnSubmit.mockImplementation(
      () => new Promise((resolve) => setTimeout(resolve, 1000))
    );
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByTestId('email-input'), '[email protected]');
    fireEvent.changeText(screen.getByTestId('password-input'), 'lozinka123');
    fireEvent.press(screen.getByTestId('submit-button'));

    expect(screen.getByTestId('loading-indicator')).toBeTruthy();
  });

  it('prikazuje grešku pri neuspješnoj prijavi', async () => {
    mockOnSubmit.mockRejectedValueOnce(new Error('Unauthorized'));
    render(<LoginForm onSubmit={mockOnSubmit} />);

    fireEvent.changeText(screen.getByTestId('email-input'), '[email protected]');
    fireEvent.changeText(screen.getByTestId('password-input'), 'kriva-lozinka');
    fireEvent.press(screen.getByTestId('submit-button'));

    await waitFor(() => {
      expect(screen.getByTestId('error-text')).toHaveTextContent(
        'Neispravni podaci za prijavu'
      );
    });
  });
});

Primijetite upotrebu testID propertija. Ovo je ključna praksa u React Native testiranju — testID se mapira na accessibilityIdentifier na iOS-u i resource-id na Androidu, što znači da iste identifikatore mogu koristiti i vaši E2E testovi. Zgodno, zar ne? Više o tome malo niže.

End-to-end testovi

E2E testovi su vrh piramide. Najskuplji su za pisanje i održavanje, najsporiji za izvršavanje, ali pokrivaju ono što nijedan drugi tip testa ne može — kompletni korisnički tok kroz stvarnu aplikaciju, na stvarnom (ili emuliranom) uređaju.

Prijava, navigacija, dodavanje u košaricu, plaćanje — cijeli scenarij od početka do kraja.

U React Native ekosustavu u 2026. godini, dva alata dominiraju ovim prostorom: Maestro i Detox. Svaki ima potpuno drugačiju filozofiju, pa ih pogledajmo detaljno.

Maestro: Moderni pristup E2E testiranju

Maestro je relativno novi igrač u svijetu mobilnog testiranja, ali je u nevjerojatno kratkom roku postao favorit mnogih React Native timova. Razlog? Maestro radi stvari drugačije. Umjesto da pišete testove u JavaScriptu ili Kotlinu, koristite deklarativnu YAML sintaksu. Umjesto da zahtijeva modifikacije koda aplikacije, radi kao potpuno crna kutija (black-box). I umjesto da se borite s nestabilnošću testova, imate ugrađenu toleranciju na flakiness.

Maestro CLI 2.2.0, najnovija verzija, donosi poboljšanu podršku za React Native, bolju integraciju s Expo ekosustavom (uključujući Expo Go i EAS Workflows), i prosječno vrijeme izvršavanja black-box testa od 12 do 18 sekundi. Za usporedbu, tipični Detox E2E test traje 30-60 sekundi. To je velika razlika kad imate dvadesetak testova.

Instalacija i postavljanje Maestra

Instalacija Maestra je gotovo smiješno jednostavna. Doslovno jedna naredba:

curl -Ls "https://get.maestro.mobile.dev" | bash

Provjerite da je sve u redu:

maestro --version
# Maestro CLI 2.2.0

I to je to. Nema npm paketa za instalirati, nema nativnih ovisnosti za konfigurirati, nema Xcode build faza za dodavati. Maestro radi izvan vaše aplikacije — jednostavno mu kažete koji .apk ili .app file da pokrene, i gotovo.

Za Expo projekte, stvar je još jednostavnija. Maestro ima potpunu kompatibilnost s Expo Go, što znači da možete testirati svoju aplikaciju bez ikakvog nativnog builda. Za produkcijsko testiranje, koristite EAS Build:

# Za iOS simulator build
eas build --profile development --platform ios --local

# Za Android emulator build
eas build --profile development --platform android --local

Pisanje prvog YAML testa

Kreirajte direktorij .maestro/ u korijenu vašeg projekta i napišite svoj prvi test:

# .maestro/login_flow.yaml
appId: com.myapp.example
---
- launchApp

# Unos korisničkih podataka
- tapOn:
    id: "email-input"
- inputText: "[email protected]"

- tapOn:
    id: "password-input"
- inputText: "MojaSigurnaLozinka123"

# Pritisak na gumb za prijavu
- tapOn:
    id: "submit-button"

# Čekanje i provjera uspješne prijave
- assertVisible:
    id: "home-screen"
    timeout: 5000

# Provjera prikazanog teksta
- assertVisible: "Dobrodošli"

Pokretanje:

maestro test .maestro/login_flow.yaml

Nekoliko stvari odmah upadaju u oči. Prvo, čitljivost — čak i netko tko nikada nije vidio Maestro može razumjeti što ovaj test radi. Drugo, Maestro automatski čeka da elementi budu vidljivi prije interakcije — nema potrebe za eksplicitnim waitFor pozivima svugdje. I treće, id u Maestru odgovara testID propertiju u React Native komponentama, što stvara prirodnu vezu između koda i testova.

Napredne značajke Maestra

Maestro nije samo za jednostavne tapove i provjere. Podržava i složenije scenarije koji su uobičajeni u stvarnim aplikacijama:

# .maestro/cart_flow.yaml
appId: com.myapp.example
---
- launchApp

# Prijava
- runFlow: login_flow.yaml

# Navigacija do proizvoda
- tapOn:
    id: "tab-products"

# Scroll do određenog proizvoda
- scrollUntilVisible:
    element:
      text: "React Native majica"
    direction: DOWN
    timeout: 10000

- tapOn: "React Native majica"

# Dodavanje u košaricu
- tapOn:
    id: "add-to-cart-button"

# Provjera badge-a na košarici
- assertVisible:
    id: "cart-badge"
    text: "1"

# Navigacija do košarice
- tapOn:
    id: "tab-cart"

# Provjera sadržaja košarice
- assertVisible: "React Native majica"
- assertVisible: "29,99 EUR"

# Screenshot za dokumentaciju
- takeScreenshot: "cart_with_item"

Maestro podržava i ponavljanje tokova (runFlow), uvjetno izvršavanje, varijable okruženja, snimanje screenshotova, pa čak i testiranje dubinskih linkova i push notifikacija. Sve to bez jedne jedine linije JavaScript koda.

Jedna od najkorisnijih značajki je ugrađena tolerancija na nestabilnost. Maestro automatski ponavlja naredbe koje ne uspiju iz prvog pokušaja — recimo, ako element nije odmah vidljiv jer se podatci još učitavaju. Ovo drastično smanjuje lažne negativne rezultate koji su inače (priznajem iz osobnog iskustva) pravo prokletstvo E2E testiranja.

Detox: Gray-box testiranje za React Native

Dok Maestro pristupa testiranju kao potpuno vanjski alat, Detox koristi drugačiji pristup — gray-box testiranje. Što to znači u praksi? Detox ima djelomičan uvid u unutarnje stanje aplikacije. Na iOS-u koristi Appleov EarlGrey framework, a na Androidu Googleov Espresso. Ovo mu omogućuje da zna kada je aplikacija "mirna" — kada su sve animacije završile, svi mrežni zahtjevi vraćeni, svi timeri završeni — i tek tada izvršava sljedeću naredbu testa.

Detox ima impresivnih 11.800+ zvjezdica na GitHubu i službeno podržava React Native verzije od 0.77.x do 0.83.x. Razvija ga Wix, koji ga koristi za testiranje vlastitih React Native aplikacija u produkciji — dakle, iza alata stoji tim koji ga i sam svakodnevno koristi.

Postavljanje Detoxa

Za razliku od Maestra, postavljanje Detoxa zahtijeva nešto više truda. To je cijena gray-box pristupa — alat mora biti integriran s nativnim buildovima vaše aplikacije:

# Globalna instalacija CLI-ja
npm install -g detox-cli

# Instalacija u projekt
npm install --save-dev detox

# Inicijalizacija konfiguracije
detox init

Nakon inicijalizacije, Detox stvara konfiguracijsku datoteku. Evo tipične konfiguracije:

// .detoxrc.js
/** @type {import('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/MojaApp.app',
      build: 'xcodebuild -workspace ios/MojaApp.xcworkspace -scheme MojaApp -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',
      reversePorts: [8081],
    },
  },
  devices: {
    simulator: {
      type: 'ios.simulator',
      device: {
        type: 'iPhone 16',
      },
    },
    emulator: {
      type: 'android.emulator',
      device: {
        avdName: 'Pixel_7_API_35',
      },
    },
  },
  configurations: {
    'ios.sim.debug': {
      device: 'simulator',
      app: 'ios.debug',
    },
    'android.emu.debug': {
      device: 'emulator',
      app: 'android.debug',
    },
  },
};

Da, konfiguracija je opširnija nego kod Maestra. Detox mora znati kako buildati vašu aplikaciju, koji uređaj koristiti, koji test runner pokrenuti. Ali zauzvrat dobivate dublje razumijevanje stanja aplikacije i precizniju sinkronizaciju — a to zna biti neprocjenjivo kod kompleksnijih scenarija.

Pisanje Detox testova

Detox testovi se pišu u JavaScriptu (ili TypeScriptu) koristeći Jest kao test runner. Sintaksa će biti poznata svakom tko je ikad pisao Jest testove:

// e2e/login.test.ts
import { by, device, element, expect } from 'detox';

describe('Tok prijave', () => {
  beforeAll(async () => {
    await device.launchApp({ newInstance: true });
  });

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

  it('trebao bi prikazati formu za prijavu', async () => {
    await expect(element(by.id('login-form'))).toBeVisible();
    await expect(element(by.id('email-input'))).toBeVisible();
    await expect(element(by.id('password-input'))).toBeVisible();
  });

  it('trebao bi prikazati grešku za prazna polja', async () => {
    await element(by.id('submit-button')).tap();
    await expect(element(by.id('error-text'))).toHaveText('Sva polja su obavezna');
  });

  it('trebao bi uspješno prijaviti korisnika', async () => {
    await element(by.id('email-input')).typeText('[email protected]');
    await element(by.id('password-input')).typeText('MojaSigurnaLozinka123');
    await element(by.id('submit-button')).tap();

    await waitFor(element(by.id('home-screen')))
      .toBeVisible()
      .withTimeout(5000);

    await expect(element(by.text('Dobrodošli'))).toBeVisible();
  });

  it('trebao bi ispravno odjaviti korisnika', async () => {
    // Prvo se prijavi
    await element(by.id('email-input')).typeText('[email protected]');
    await element(by.id('password-input')).typeText('MojaSigurnaLozinka123');
    await element(by.id('submit-button')).tap();

    await waitFor(element(by.id('home-screen')))
      .toBeVisible()
      .withTimeout(5000);

    // Odjava
    await element(by.id('settings-tab')).tap();
    await element(by.id('logout-button')).tap();

    // Provjera da smo na ekranu za prijavu
    await expect(element(by.id('login-form'))).toBeVisible();
  });
});

Pokretanje Detox testova zahtijeva dva koraka — build pa test:

# Build aplikacije za testiranje
detox build --configuration ios.sim.debug

# Pokretanje testova
detox test --configuration ios.sim.debug

Ključna prednost Detoxa je automatska sinkronizacija. Detox zna kada React Native bridge (ili JSI) miruje, kada su animacije završile, kada su mrežni zahtjevi završeni. To znači manje eksplicitnih čekanja i stabilnije testove — barem u teoriji. U praksi, složeni scenariji s WebSocketovima ili beskonačnim animacijama (looking at you, Lottie) mogu zahtijevati ručno upravljanje sinkronizacijom.

Maestro vs Detox: Koji odabrati?

Ovo je pitanje koje čujem najčešće, i odgovor — kao i uvijek u softverskom inženjerstvu — je: ovisi. Ali umjesto prazne floskule, evo konkretne usporedbe:

Značajka Maestro Detox
Pristup testiranju Black-box Gray-box
Sintaksa testova YAML (deklarativna) JavaScript/TypeScript
Modifikacija koda aplikacije Nije potrebna Potrebni nativni build hookovi
Vrijeme postavljanja ~5 minuta 30-60 minuta
Prosječno trajanje testa 12-18 sekundi 30-60 sekundi
Tolerancija na nestabilnost Ugrađena Automatska sinkronizacija
Expo podrška Expo Go + EAS Workflows Samo bare RN i EAS prebuild
iOS mehanizam XCUITest EarlGrey
Android mehanizam UIAutomator Espresso
GitHub zvjezdice ~6.500+ 11.800+
Krivulja učenja Niska Srednja do visoka
Prilagodljivost Umjerena Visoka
Testiranje na pravim uređajima Da (Maestro Cloud) Da (ograničeno)

Odaberite Maestro ako:

  • Koristite Expo i želite brzo postaviti E2E testove
  • Vaš tim ima QA inženjere koji nisu nužno JavaScript developeri
  • Želite najbrži put od nule do funkcionalnih E2E testova
  • Ne želite dirati nativni kod aplikacije
  • Nestabilnost testova vam je bio problem s drugim alatima

Odaberite Detox ako:

  • Trebate duboku integraciju s nativnim slojem
  • Imate složene scenarije koji zahtijevaju programsku logiku u testovima
  • Vaš tim je udoban s JavaScript/TypeScript testovima
  • Trebate preciznu kontrolu nad sinkronizacijom i stanjem aplikacije
  • Radite na bare React Native projektu (bez Expo-a)

Moje mišljenje? Za većinu timova u 2026., posebno onih koji koriste Expo, Maestro je pragmatičniji izbor. Brže postavljanje, jednostavnija sintaksa i ugrađena tolerancija na nestabilnost znače da ćete više vremena provoditi pišući testove, a manje se boreći s infrastrukturom. Ali ako vam treba moć i fleksibilnost gray-box pristupa, Detox je i dalje odličan alat — samo budite spremni uložiti malo više vremena u postavljanje.

Integracija testova u CI/CD pipeline

Testovi koji se pokreću samo na lokalnom računalu developera nisu pravi testovi — to su, hajmo reći, nade i molitve. Da bi testovi stvarno služili svojoj svrsi, moraju se izvršavati automatski pri svakom pull requestu i svakom pushu na main granu.

Evo kompletnog primjera GitHub Actions workflowa koji pokreće sve tri razine testova:

# .github/workflows/test.yml
name: React Native Test Suite

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  unit-and-component-tests:
    name: Unit & Component Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run Jest tests
        run: npx jest --coverage --ci --reporters=default --reporters=jest-junit
        env:
          JEST_JUNIT_OUTPUT_DIR: ./reports

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage/lcov.info

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: ./reports

  e2e-tests-ios:
    name: Maestro E2E Tests (iOS)
    runs-on: macos-14
    needs: unit-and-component-tests
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Maestro CLI
        run: curl -Ls "https://get.maestro.mobile.dev" | bash

      - name: Add Maestro to PATH
        run: echo "$HOME/.maestro/bin" >> $GITHUB_PATH

      - name: Build iOS app
        run: npx expo run:ios --configuration Release --no-install

      - name: Boot iOS Simulator
        run: |
          xcrun simctl boot "iPhone 16"
          xcrun simctl install booted build/MojaApp.app

      - name: Run Maestro E2E tests
        run: |
          maestro test .maestro/ \
            --format junit \
            --output reports/maestro-results.xml

      - name: Upload E2E results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: e2e-results
          path: ./reports/maestro-results.xml

      - name: Upload E2E screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: e2e-screenshots
          path: ./.maestro/screenshots/

  e2e-tests-android:
    name: Maestro E2E Tests (Android)
    runs-on: ubuntu-latest
    needs: unit-and-component-tests
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'zulu'
          java-version: '17'

      - name: Install Maestro CLI
        run: curl -Ls "https://get.maestro.mobile.dev" | bash

      - name: Add Maestro to PATH
        run: echo "$HOME/.maestro/bin" >> $GITHUB_PATH

      - name: Build Android APK
        run: |
          cd android
          ./gradlew assembleDebug

      - name: Run Maestro E2E tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 35
          arch: x86_64
          script: |
            adb install android/app/build/outputs/apk/debug/app-debug.apk
            maestro test .maestro/ \
              --format junit \
              --output reports/maestro-results.xml

Ovaj workflow radi sljedeće: prvo pokreće unit i component testove na jeftinom Ubuntu runneru. Tek ako oni prođu, pokreću se E2E testovi na macOS runneru (za iOS) i Ubuntu runneru s Android emulatorom. Ova sekvencijalna strategija štedi minute CI vremena — jer nema nikakvog smisla pokretati skupe E2E testove ako vam unit testovi padaju.

Praktični savjeti za stabilne testove

Nakon godina rada s React Native testiranjem, evo savjeta koji su mi osobno uštedili nebrojene sate frustracije:

1. Koristite konzistentnu konvenciju za testID

Definirajte konvenciju imenovanja i držite je se u cijelom projektu. Preporučam kebab-case s hijerarhijskom strukturom:

// Dobro - jasno i konzistentno
<View testID="login-screen">
  <TextInput testID="login-email-input" />
  <TextInput testID="login-password-input" />
  <Pressable testID="login-submit-button" />
</View>

// Loše - nekonzistentno i nejasno
<View testID="loginView">
  <TextInput testID="email" />
  <TextInput testID="pwd_input" />
  <Pressable testID="btn1" />
</View>

2. Izbjegavajte testiranje implementacijskih detalja

Testirajte što korisnik vidi i radi, ne kako vaša komponenta interno funkcionira. Ako promijenite state management s useReducer na Zustand, testovi ne bi smjeli pasti — jer se korisničko iskustvo nije promijenilo.

3. Držite E2E testove kratkim i fokusiranim

Svaki E2E test trebao bi testirati jedan korisnički tok. Nemojte pisati test koji registrira korisnika, prijavljuje ga, dodaje proizvod u košaricu, plaća i odjavljuje se — to je pet testova, ne jedan. Kad srednji korak padne, nećete imati pojma koji dio je pokvaren.

4. Koristite factory funkcije za testne podatke

// testUtils/factories.ts
export function createMockUser(overrides = {}) {
  return {
    id: 'test-user-1',
    name: 'Test Korisnik',
    email: '[email protected]',
    avatar: 'https://placekitten.com/200/200',
    ...overrides,
  };
}

export function createMockProduct(overrides = {}) {
  return {
    id: 'prod-1',
    name: 'React Native majica',
    price: 29.99,
    currency: 'EUR',
    inStock: true,
    ...overrides,
  };
}

5. Nemojte koristiti fiksna čekanja

Umjesto await new Promise(r => setTimeout(r, 3000)), koristite waitFor u Detoxu ili pustite Maestro da sam čeka. Fiksna čekanja su najčešći uzrok nestabilnih testova — na sporom CI serveru 3 sekunde možda nisu dovoljne, a na brzom ste nepotrebno usporili cijeli suite.

6. Resetirajte stanje između testova

Ovo je kritično. Svaki test mora biti neovisan. U Detoxu koristite device.reloadReactNative() ili device.launchApp({ delete: true }). U Maestru, svaki YAML file počinje s launchApp što automatski resetira stanje. U Jest/RNTL testovima, koristite cleanup() i resetirajte mockove u beforeEach.

7. Testirajte na obje platforme

Zvuči očigledno, ali iznenadili biste se koliko timova testira samo na iOS-u jer je "brže postaviti simulator". React Native obećava cross-platform razvoj, ali to ne znači identično ponašanje. Geste, tastatura, navigacija, scroll — sve je subtilno drugačije između iOS-a i Androida. Pokrenite E2E testove na obje platforme u CI-ju. Budući vi će vam biti zahvalni.

Često postavljana pitanja (FAQ)

Koji je najbolji alat za testiranje React Native aplikacija u 2026?

Ne postoji jedan "najbolji" alat — postoji najbolja kombinacija alata. Slojeviti pristup daje najbolje rezultate: Jest za unit testove, React Native Testing Library za testove komponenata, i Maestro ili Detox za E2E testove. Za većinu timova, posebno onih koji koriste Expo, preporučam kombinaciju Jest + RNTL + Maestro.

Mogu li koristiti Maestro s Expo projektima?

Da, i to izvrsno. Maestro ima potpunu kompatibilnost s Expo Go za razvoj i testiranje, te s EAS Workflows za CI/CD integraciju. Ne trebate raditi prebuild niti eject iz Expo ekosustava. Jednostavno instalirate Maestro, napišete YAML testove i pokrenete ih protiv vaše Expo aplikacije. To je zapravo jedna od najvećih prednosti Maestra — ne zahtijeva nikakve modifikacije koda, pa radi s bilo kojim React Native projektom.

Koliko je teško postaviti Detox za React Native?

Da budem iskren, postavljanje Detoxa nije trivijalno. Zahtijeva konfiguraciju nativnih buildova (Xcode projekt za iOS, Gradle za Android), instalaciju globalnog CLI-ja i lokalnog paketa, te razumijevanje build procesa vaše aplikacije. Za iskusne React Native developere koji rade s bare projektima, to je posao od 30-60 minuta. Za Expo projekte ili početnike, može potrajati i duže jer Detox zahtijeva nativne build hookove — morate imati pristup nativnom kodu.

Trebam li testirati na pravim uređajima ili emulatorima?

Za svakodnevni razvoj i CI/CD, emulatori i simulatori su sasvim dovoljni. Brži su, besplatni su, i pokrivaju 95% scenarija. Međutim, prije velikih izdanja, preporučam testiranje na pravim uređajima — posebno za performanse, geste, kameru, biometriju i specifičnosti pojedinih proizvođača. Samsung, Xiaomi, Huawei uređaji ponekad imaju svoje specifično ponašanje koje simulator neće uhvatiti. Maestro Cloud nudi testiranje na pravim uređajima u oblaku, što eliminira potrebu za fizičkom farmom uređaja.

Kako smanjiti flakiness u E2E testovima?

Nestabilnost E2E testova je vječni problem mobilnog testiranja. Evo što konkretno pomaže:

  • Koristite Maestro — ima ugrađenu toleranciju na nestabilnost s automatskim ponovnim pokušajima
  • Izbjegavajte fiksna čekanja — koristite waitFor i uvjete umjesto sleep(3000)
  • Resetirajte stanje — svaki test počinje od čistog stanja aplikacije
  • Koristite testID umjesto teksta — tekst se mijenja s lokalizacijom, ID-evi ne
  • Stabilizirajte testne podatke — koristite mock API ili seedane podatke, ne produkcijsku bazu
  • Izolirajte mrežne pozive — koristite mock server za API pozive u E2E testovima
  • Pokrenite retry na CI-ju — konfiguracija poput retry: 2 u GitHub Actions pomaže s rijetkim infrastrukturnim problemima

Na kraju, najvažnija stvar kod testiranja nije alat koji koristite, nego da uopće testirate. Pet dobro napisanih testova koji se izvršavaju u CI-ju pri svakom pull requestu vrijede više od stotinu testova koje nitko ne pokreće. Počnite malo, budite konzistentni, i postepeno gradite povjerenje u svoju test infrastrukturu. Vaš budući ja — onaj koji u petak poslijepodne radi deploy — bit će vam zahvalan.

O Autoru Editorial Team

Our team of expert writers and editors.