Integration Test #

Integration test menjalankan aplikasi yang sesungguhnya di device atau emulator nyata, mensimulasikan alur pengguna dari awal sampai akhir. Flutter memiliki package integration_test bawaan, tapi ia memiliki keterbatasan fundamental: tidak bisa berinteraksi dengan komponen native seperti dialog izin lokasi/kamera, WebView untuk OAuth, atau notifikasi sistem. Patrol dari LeanCode hadir untuk mengatasi keterbatasan ini.

Patrol vs integration_test #

integration_test (bawaan Flutter):
  ✓ Tidak butuh setup tambahan
  ✓ Bisa test widget Flutter
  ✗ TIDAK bisa: dialog izin native (lokasi, kamera, notifikasi)
  ✗ TIDAK bisa: WebView (Google Sign-In, OAuth)
  ✗ TIDAK bisa: notifikasi sistem, settings OS
  ✗ TIDAK bisa: tekan tombol home/back hardware

Patrol (LeanCode):
  ✓ Semua kemampuan integration_test
  ✓ Interaksi dialog izin native
  ✓ Buka/tutup notifikasi, WebView
  ✓ Kontrol WiFi, dark mode, orientation
  ✓ Hot Restart -- test lebih cepat dikembangkan
  ✓ Finder yang lebih ringkas ($)
  ✓ Isolasi test yang lebih baik

Instalasi Patrol #

# pubspec.yaml
dev_dependencies:
  patrol: ^3.14.0
  integration_test:
    sdk: flutter
# Install Patrol CLI (wajib untuk native automation)
dart pub global activate patrol_cli

# Verifikasi instalasi
patrol --version

Setup Android #

// android/app/src/androidTest/java/com/namapackage/MainActivityTest.java
package com.namapackage;

import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import pl.leancode.patrol.PatrolJUnitRunner;

@RunWith(Parameterized.class)
public class MainActivityTest {
  @Parameterized.Parameters(name = "{0}")
  public static Object[] testCases() {
    PatrolJUnitRunner instrumentation =
        (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
    instrumentation.setUp(MainActivity.class);
    instrumentation.waitForPatrolAppService();
    return instrumentation.listDartTests();
  }

  public MainActivityTest(String dartTestName) {
    this.dartTestName = dartTestName;
  }

  private final String dartTestName;

  @org.junit.Test
  public void runDartTest() {
    PatrolJUnitRunner instrumentation =
        (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
    instrumentation.runDartTest(dartTestName);
  }
}
// android/app/build.gradle -- tambahkan di defaultConfig
defaultConfig {
  testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner"
  testInstrumentationRunnerArguments["clearPackageData"] = "true"
}

// dan di android block
testOptions {
  execution "ANDROIDX_TEST_ORCHESTRATOR"
}

// di dependencies
dependencies {
  androidTestUtil "androidx.test:orchestrator:1.4.2"
}

Setup iOS #

# ios/Podfile -- pastikan minimum iOS 16
platform :ios, '16.0'
// ios/RunnerUITests/RunnerUITests.swift
import XCTest

final class RunnerUITests: XCTestCase {
  func testRunner() throws {
    let app = XCUIApplication()
    app.launch()
  }
}

Struktur File Test #

integration_test/
  flows/
    auth/
      login_test.dart         ← alur login
      register_test.dart      ← alur registrasi
    produk/
      browse_produk_test.dart ← browse dan filter produk
      detail_produk_test.dart ← halaman detail
    checkout/
      checkout_flow_test.dart ← alur checkout lengkap
  helpers/
    app_runner.dart           ← setup dan teardown app
    mock_server.dart          ← mock API untuk test
  pages/                      ← Page Object Model
    login_page.dart
    home_page.dart
    produk_page.dart

patrolTest — Dasar Penulisan Test #

// integration_test/flows/auth/login_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
import 'package:myapp/main.dart' as app;

void main() {
  // patrolTest menggantikan testWidgets untuk integration test
  patrolTest(
    'pengguna bisa login dengan email dan password',
    ($) async {
      // Jalankan aplikasi
      await $.pumpWidgetAndSettle(app.MyApp());

      // Patrol menggunakan $ sebagai shortcut untuk interaksi
      // $('teks') setara dengan find.text('teks') tapi lebih ringkas

      // Input email -- cari TextField berdasarkan key atau semantics
      await $(#emailInput).enterText('[email protected]');
      await $.pump();

      // Input password
      await $(#passwordInput).enterText('password123');
      await $.pump();

      // Tap tombol login
      await $(#loginButton).tap();
      await $.pumpAndSettle();

      // Verifikasi berada di HomeScreen
      expect($(#homeScreen), findsOneWidget);
      expect($('Selamat datang, Budi!'), findsOneWidget);
    },
  );

  patrolTest(
    'login gagal menampilkan pesan error',
    ($) async {
      await $.pumpWidgetAndSettle(app.MyApp());

      await $(#emailInput).enterText('[email protected]');
      await $(#passwordInput).enterText('passwordsalah');
      await $(#loginButton).tap();
      await $.pumpAndSettle();

      // Masih di LoginScreen
      expect($(#loginScreen), findsOneWidget);
      // Error message muncul
      expect($('Email atau password salah'), findsOneWidget);
    },
  );
}

Patrol Custom Finder — $ Operator #

// $ adalah PatrolTester yang disediakan oleh patrolTest
// Cara menggunakan $:

// Berdasarkan ValueKey (gunakan # untuk Symbol)
await $(#loginButton).tap();           // find.byKey(const ValueKey('loginButton'))

// Berdasarkan tipe widget
await $(ElevatedButton).tap();         // find.byType(ElevatedButton)

// Berdasarkan teks
await $('Submit').tap();               // find.text('Submit')

// Berdasarkan icon
await $(Icons.add).tap();

// Chaining -- cari di dalam parent
await $(#produkList).$(ListTile).tap();  // ListTile di dalam #produkList

// Menunggu sampai widget muncul (lebih andal dari pumpAndSettle untuk async)
await $(#homeScreen).waitUntilVisible();
await $(#loadingIndicator).waitUntilGone();

Interaksi Native — Izin dan OS #

Inilah keunggulan utama Patrol dibanding integration_test biasa:

patrolTest(
  'app meminta izin lokasi dan menampilkan peta setelah diizinkan',
  ($) async {
    await $.pumpWidgetAndSettle(app.MyApp());

    // Navigasi ke fitur yang butuh izin lokasi
    await $('Cari Toko Terdekat').tap();
    await $.pumpAndSettle();

    // Dialog izin native muncul secara otomatis
    // Patrol bisa mengaksepnya!
    if (await $.native.isPermissionDialogVisible()) {
      await $.native.grantPermissionWhenInUse();  // atau denyPermission()
    }

    await $.pumpAndSettle();

    // Setelah izin diberikan, peta seharusnya muncul
    expect($(#mapView), findsOneWidget);
  },
);

patrolTest(
  'app meminta izin notifikasi saat onboarding',
  ($) async {
    await $.pumpWidgetAndSettle(app.MyApp());

    // Lewati onboarding
    await $('Mulai').tap();
    await $.pumpAndSettle();

    // Patrol menerima dialog notifikasi
    if (await $.native.isPermissionDialogVisible()) {
      await $.native.grantPermissionWhenInUse();
    }

    // Interaksi dengan notifikasi native
    await $.native.openNotifications();

    // Verifikasi notifikasi dari app kita ada
    expect(await $.native.containsNotificationWhere(
      appName: 'MyApp',
      titleContains: 'Selamat datang',
    ), isTrue);

    await $.native.tapOnNotificationByIndex(0);
    await $.pumpAndSettle();

    // App terbuka ke halaman yang sesuai setelah tap notifikasi
    expect($(#homeScreen), findsOneWidget);
  },
);

patrolTest(
  'app berfungsi saat WiFi dimatikan',
  ($) async {
    await $.pumpWidgetAndSettle(app.MyApp());
    await $.pumpAndSettle();

    // Navigasi ke halaman produk
    await $('Produk').tap();
    await $.pumpAndSettle();

    // Matikan WiFi
    await $.native.disableWifi();
    await $.pump(const Duration(seconds: 1));

    // Refresh
    await $(#refreshButton).tap();
    await $.pumpAndSettle();

    // Tampilkan error offline
    expect($('Tidak ada koneksi internet'), findsOneWidget);

    // Nyalakan kembali WiFi
    await $.native.enableWifi();
    await $.pumpAndSettle();
  },
);

Page Object Model — Struktur Test yang Bersih #

Page Object Model (POM) memisahkan logika interaksi dari logika test, sehingga jika UI berubah hanya satu file yang perlu diupdate:

// integration_test/pages/login_page.dart
class LoginPage {
  final PatrolTester $;
  LoginPage(this.$);

  // Finders
  PatrolFinder get emailField => $(#emailInput);
  PatrolFinder get passwordField => $(#passwordInput);
  PatrolFinder get loginButton => $(#loginButton);
  PatrolFinder get errorMessage => $(#errorMessage);
  PatrolFinder get googleLoginButton => $(#googleLogin);

  // Actions
  Future<void> enterEmail(String email) async {
    await emailField.enterText(email);
    await $.pump();
  }

  Future<void> enterPassword(String password) async {
    await passwordField.enterText(password);
    await $.pump();
  }

  Future<void> tapLogin() async {
    await loginButton.tap();
    await $.pumpAndSettle();
  }

  Future<void> login(String email, String password) async {
    await enterEmail(email);
    await enterPassword(password);
    await tapLogin();
  }

  // Assertions
  void verifyOnLoginPage() {
    expect($(#loginScreen), findsOneWidget);
  }

  void verifyErrorShown(String message) {
    expect($(message), findsOneWidget);
  }
}

// integration_test/pages/home_page.dart
class HomePage {
  final PatrolTester $;
  HomePage(this.$);

  void verifyOnHomePage() {
    expect($(#homeScreen), findsOneWidget);
  }

  Future<void> navigateToProduk() async {
    await $('Produk').tap();
    await $.pumpAndSettle();
  }
}

// Penggunaan di test -- jauh lebih bersih dan mudah dibaca
patrolTest('alur login berhasil', ($) async {
  await $.pumpWidgetAndSettle(app.MyApp());

  final loginPage = LoginPage($);
  final homePage = HomePage($);

  await loginPage.login('[email protected]', 'password123');
  homePage.verifyOnHomePage();
});

patrolTest('login dengan email kosong menampilkan error', ($) async {
  await $.pumpWidgetAndSettle(app.MyApp());

  final loginPage = LoginPage($);

  await loginPage.login('', 'password123');
  loginPage.verifyErrorShown('Email tidak boleh kosong');
  loginPage.verifyOnLoginPage();
});

Menjalankan Patrol Test #

# Jalankan semua integration test
patrol test

# Jalankan file test tertentu
patrol test -t integration_test/flows/auth/login_test.dart

# Jalankan di device tertentu
patrol test --device emulator-5554

# Hot Restart mode -- untuk development test (lebih cepat)
patrol develop -t integration_test/flows/auth/login_test.dart

# Build tanpa jalankan (untuk upload ke test farm)
patrol build android
patrol build ios

Integrasi CI — GitHub Actions #

# .github/workflows/integration_test.yml
name: Integration Tests

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

jobs:
  integration-test-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.x'

      - name: Install Patrol CLI
        run: dart pub global activate patrol_cli

      - name: Enable KVM (untuk emulator Android)
        run: |
          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm          

      - name: Start Android Emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 33
          arch: x86_64
          script: patrol test -t integration_test/flows/auth/login_test.dart

Ringkasan #

  • Integration test menjalankan aplikasi sesungguhnya di device/emulator — paling lambat tapi paling mirip dengan pengguna nyata.
  • Gunakan Patrol (bukan integration_test bawaan) untuk bisa berinteraksi dengan dialog izin native, WebView, notifikasi sistem, dan kontrol OS.
  • patrolTest() menggantikan testWidgets() untuk integration test. Parameter $ adalah PatrolTester yang menyediakan finder ringkas.
  • $ operator adalah finder ringkas Patrol: $(#keyName) untuk ValueKey, $(WidgetType) untuk tipe, $('teks') untuk teks.
  • Untuk interaksi native (izin, notifikasi): $.native.grantPermissionWhenInUse(), $.native.openNotifications(), $.native.disableWifi().
  • Gunakan Page Object Model — pisahkan logika interaksi ke class terpisah agar perubahan UI hanya mempengaruhi satu file, bukan semua test.
  • Integration test hanya untuk alur kritis (login, checkout, pembayaran) — jangan test setiap fitur dengan integration test, biaya dan lambatnya tidak sebanding.
  • Gunakan patrol develop mode saat menulis test — Hot Restart membuat siklus development jauh lebih cepat.

← Sebelumnya: Widget Test   Berikutnya: Best Practice →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact