Integration Test #
Lapisan teratas dari piramida pengujian kita adalah integration test (atau sering disebut pengujian end-to-end / E2E). Berbeda dengan unit test dan widget test yang mensimulasikan logika dan UI di memori RAM terisolasi, integration test menjalankan aplikasi kita yang sesungguhnya di dalam perangkat riil, emulator Android, atau simulator iOS. Pengujian ini bertugas meniru perilaku pengguna nyata secara presisi: mengetuk layar, menunggu respon API server backend yang asli, membaca database fisik, hingga berinteraksi dengan komponen native di luar Flutter.
Flutter menyediakan paket bawaan bernama integration_test, namun paket tersebut memiliki keterbatasan fundamental: ia hanya dapat mengontrol elemen yang berada di dalam Flutter view canvas. Ketika aplikasi kita memicu sistem operasi native — seperti memunculkan dialog izin kamera, meminta akses lokasi, membuka WebView untuk login OAuth pihak ketiga, atau menampilkan notifikasi sistem di laci notifikasi (notification drawer) — integration test bawaan Flutter akan macet karena tidak memiliki akses ke sistem operasi native. Untuk mengatasi keterbatasan kritis ini, kita menggunakan Patrol, sebuah kerangka kerja (framework) pengujian integrasi modern yang sangat tangguh yang dikembangkan oleh LeanCode.
Perbandingan: Patrol vs integration_test #
Sebelum kita masuk ke langkah konfigurasi, sangat penting untuk memahami mengapa Patrol adalah pilihan standar industri untuk pengujian integrasi aplikasi Flutter berskala produksi:
- Penyaringan Native Dialog: Patrol memiliki kemampuan mengontrol elemen antarmuka native OS. Kita dapat menerima (grant) atau menolak (deny) dialog izin lokasi, kamera, galeri foto, atau notifikasi sistem secara otomatis saat tes berjalan.
- Akses WebView: Sangat berguna jika alur autentikasi aplikasi kita menggunakan Google Sign-In, Apple Sign-In, atau gerbang pembayaran (payment gateway) berbasis web. Patrol dapat masuk ke dalam WebView native dan mengetikkan kredensial di sana.
- Interaksi Sistem Operasi: Kita dapat mematikan/menghidupkan jaringan WiFi, merubah orientasi layar (potrait/landscape), membuka laci notifikasi sistem, hingga menekan tombol perangkat keras native seperti tombol Home atau Back.
- Penyederhanaan Sintaks Finder (
$): Patrol membungkus Finder bawaan Flutter ke dalam sintaksis yang jauh lebih ringkas, mempercepat penulisan kode tes dan mengurangi kebisingan baris kode (boilerplate code).
Arsitektur Runner: Patrol vs integration_test Bawaan #
Salah satu alasan mengapa Patrol mampu mengendalikan elemen native sedangkan integration_test bawaan tidak bisa terletak pada desain arsitektur eksekusi tesnya. Patrol menggunakan pendekatan Multi-Runner dengan menghubungkan Dart Test Runner di dalam aplikasi dengan Native UI Test Runner tingkat OS (UIAutomator di Android dan XCTest di iOS) melalui server HTTP lokal.
graph TD
subgraph Patrol["Arsitektur Patrol (Multi-Runner)"]
DartRunner["Dart Test Runner (Flutter VM)"] -->|Komunikasi RPC (Local Host)| AppService["Patrol App Service (Local HTTP Server)"]
AppService -->|Perintah Native| NativeRunner["Native UI Test Runner (UIAutomator / XCTest)"]
NativeRunner -->|Interaksi Sistem| OS["Sistem Operasi (OS Android / iOS)"]
DartRunner -->|Interaksi UI Flutter| FlutterUI["Flutter Engine View"]
end
subgraph BuiltIn["Arsitektur integration_test (Bawaan)"]
BuiltInDart["Dart Test Runner (Flutter VM)"] -->|Interaksi Terbatas| FlutterUI2["Flutter Engine View"]
BuiltInDart -.->|TIDAK BISA AKSES| OS2["Sistem Operasi (OS Android / iOS)"]
endDengan memahami arsitektur di atas, kita dapat melihat bahwa Patrol bertindak sebagai jembatan cerdas yang menyatukan dunia Flutter dengan dunia native OS secara real-time selama proses pengujian berjalan.
Panduan Instalasi & Konfigurasi Platform #
Menyiapkan pengujian integrasi native memerlukan beberapa langkah konfigurasi pada tingkat proyek Android dan iOS agar instrumen pengujian OS diizinkan untuk mengendalikan aplikasi kita.
1. Konfigurasi Dependensi Dart #
Tambahkan pustaka Patrol pada bagian dev_dependencies di dalam berkas pubspec.yaml kita:
dev_dependencies:
flutter_test:
sdk: flutter
integration_test:
sdk: flutter
patrol: ^3.14.0
Selanjutnya, kita wajib menginstal Patrol CLI secara global di komputer pengembangan kita. Patrol CLI bertugas mengoordinasikan proses kompilasi native dan menjalankan native runners:
# Instal Patrol CLI secara global
dart pub global activate patrol_cli
# Verifikasi kesuksesan instalasi
patrol --version
2. Konfigurasi Proyek Android #
Pertama, buat berkas uji native Android di dalam direktori android/app/src/androidTest/java/com/example/myapp/MainActivityTest.java (ganti com/example/myapp sesuai dengan ID paket aplikasi kita):
package com.example.myapp; // Sesuaikan dengan package name kita
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);
}
}
Kedua, sesuaikan berkas konfigurasi Gradle aplikasi di android/app/build.gradle:
android {
defaultConfig {
// Tentukan runner instrumen uji menggunakan JUnit Runner milik Patrol
testInstrumentationRunner "pl.leancode.patrol.PatrolJUnitRunner"
testInstrumentationRunnerArguments["clearPackageData"] = "true"
}
testOptions {
// Menggunakan AndroidX Test Orchestrator untuk mengisolasi masing-masing tes
execution "ANDROIDX_TEST_ORCHESTRATOR"
}
}
dependencies {
// Tambahkan dependensi orchestrator di tingkat Android
androidTestUtil "androidx.test:orchestrator:1.4.2"
}
3. Konfigurasi Proyek iOS #
Untuk iOS, kita perlu memastikan target minimum sistem operasi adalah iOS 16.0 pada berkas ios/Podfile:
platform :ios, '16.0'
Kemudian, buka proyek iOS kita menggunakan Xcode (ios/Runner.xcworkspace). Tambahkan target UI Testing baru dengan nama RunnerUITests:
- Pilih File > New > Target…
- Cari dan pilih iOS UI Testing Bundle, klik Next.
- Beri nama target:
RunnerUITests, pastikan bahasa yang dipilih adalah Swift, dan klik Finish.
Buat berkas kode pengujian UI di ios/RunnerUITests/RunnerUITests.swift:
import XCTest
import patrol
class RunnerUITests: XCTestCase {
func testRunner() {
// Inisialisasi runner Patrol untuk iOS
let app = XCUIApplication()
let manager = PatrolUITestsManager(app: app)
manager.setup()
manager.run()
}
}
Dasar Penulisan Test dengan patrolTest & Kustom Finder ($) #
Setelah proses instalasi selesai, kita siap menulis kode pengujian integrasi pertama kita. Di Patrol, kita tidak lagi menggunakan fungsi testWidgets(), melainkan menggunakan fungsi khusus patrolTest() yang memberikan parameter instansiasi objek PatrolTester (yang disimbolkan dengan karakter $).
Mari kita lihat perbandingan penulisan alur login antara integration_test biasa dengan Patrol:
// integration_test/flows/auth/login_flow_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
import 'package:flutter_app/main.dart' as app; // Impor fungsi main aplikasi kita
void main() {
// Pemicu inisialisasi binding khusus Patrol
patrolTest(
'Alur login sukses dan masuk ke halaman beranda',
($) async {
// 1. Jalankan aplikasi Flutter
await $.pumpWidgetAndSettle(app.MyApp());
// 2. Gunakan operator $ untuk mencari elemen secara ringkas
// Sintaks $(#key) setara dengan find.byKey(const ValueKey('key'))
await $(#emailInputKey).enterText('[email protected]');
await $.pump();
await $(#passwordInputKey).enterText('passwordMulus123');
await $.pump();
// Tap tombol login berdasarkan teks string
await $('MASUK').tap();
// 3. Gunakan waitUntilVisible() untuk sinkronisasi asinkron yang andal
// Metode ini jauh lebih kokoh dibanding pumpAndSettle() karena
// mendeteksi kemunculan widget secara aktif meskipun ada proses loading di latar belakang.
await $(#homeScreenKey).waitUntilVisible();
// 4. Lakukan verifikasi hasil akhir
expect($('Selamat Datang, Budi!'), findsOneWidget);
expect($(#loginFormKey), findsNothing);
},
);
}
Panduan Ringkas Penggunaan Operator $:
#
$('Teks'): Mencari widget Text yang menampilkan string'Teks'.$(#inputKey): Mencari widget berdasarkanValueKey('inputKey')(menggunakan simbol#).$(ElevatedButton): Mencari widget berdasarkan tipe kelasElevatedButton.$(#parentKey).$(ListTile): Mencari widgetListTileyang berada di dalam widget induk dengan keyparentKey(chaining finder).
Otomatisasi Elemen Native OS (Izin & Notifikasi) #
Kemampuan utama yang membuat Patrol sangat populer adalah kemampuan interaksinya dengan sistem operasi native. Berikut adalah contoh pengujian alur penjemputan lokasi GPS yang memerlukan izin akses lokasi native, serta pengujian penerimaan Push Notification:
// integration_test/flows/features/gps_location_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
import 'package:flutter_app/main.dart' as app;
void main() {
patrolTest(
'Pengguna memberikan izin GPS dan aplikasi memuat peta koordinat',
($) async {
await $.pumpWidgetAndSettle(app.MyApp());
// 1. Ketuk tombol untuk masuk ke fitur navigasi peta
await $('Buka Peta Toko').tap();
await $.pumpAndSettle();
// 2. Tangani dialog izin native sistem operasi (Android / iOS)
// Patrol mendeteksi kemunculan dialog native tingkat OS secara transparan
if (await $.native.isPermissionDialogVisible()) {
// Setujui izin lokasi "When in Use" secara otomatis
await $.native.grantPermissionWhenInUse();
}
await $.pumpAndSettle();
// 3. Verifikasi peta berhasil dimuat di UI Flutter setelah izin diberikan
expect($(#mapWidgetKey), findsOneWidget);
},
);
patrolTest(
'Aplikasi menerima push notification dan merespons ketukan notifikasi',
($) async {
await $.pumpWidgetAndSettle(app.MyApp());
// Kirim trigger atau simulasikan event yang memicu push notification dari server
await $('Trigger Promo Baru').tap();
await $.pumpAndSettle();
// 1. Buka laci notifikasi sistem operasi native
await $.native.openNotifications();
// 2. Periksa apakah ada notifikasi masuk dari aplikasi kita
final bool adaNotif = await $.native.containsNotificationWhere(
appName: 'Toko Kita',
titleContains: 'Diskon Spesial',
);
expect(adaNotif, isTrue);
// 3. Ketuk notifikasi pertama yang masuk
await $.native.tapOnNotificationByIndex(0);
await $.pumpAndSettle();
// 4. Verifikasi aplikasi terbuka ke halaman detail promo
expect($(#promoDetailScreenKey), findsOneWidget);
},
);
}
Melalui metode $.native, kita dapat menguji kasus-kasus integrasi nyata yang sebelumnya mustahil diuji secara otomatis di Flutter.
Struktur Kode Bersih dengan Page Object Model (POM) #
Menulis seluruh logika pencarian Finder dan interaksi langsung di dalam berkas tes integrasi akan membuat berkas tersebut sangat panjang dan sulit dirawat. Jika suatu hari desainer UI mengubah tata letak halaman (misalnya mengubah tombol ValueKey('loginButton') menjadi ValueKey('submitButton')), kita harus mengubah semua berkas tes integrasi yang menyentuh tombol tersebut.
Untuk mengatasi masalah pemeliharaan ini, kita wajib menggunakan pola Page Object Model (POM). POM adalah pola desain di mana kita memisahkan logika interaksi UI ke dalam kelas khusus (Page Class) yang mewakili satu halaman visual aplikasi. Berkas tes kita hanya akan memanggil metode dari Page Class tersebut.
Berikut adalah implementasi POM untuk halaman Login dan Beranda:
// integration_test/pages/login_page.dart
import 'package:patrol/patrol.dart';
import 'package:flutter_test/flutter_test.dart';
class LoginPage {
final PatrolTester $;
// Konstruktor menerima instansiasi tester
LoginPage(this.$);
// 1. Definisikan semua Finder sebagai Getter terpusat
PatrolFinder get emailField => $(#emailInputKey);
PatrolFinder get passwordField => $(#passwordInputKey);
PatrolFinder get submitButton => $(#loginButtonKey);
PatrolFinder get errorMessage => $('Email atau password salah');
// 2. Definisikan semua Aksi (Actions) sebagai fungsi asinkron
Future<void> enterCredentials(String email, String password) async {
await emailField.enterText(email);
await $.pump();
await passwordField.enterText(password);
await $.pump();
}
Future<void> submitLoginForm() async {
await submitButton.tap();
await $.pumpAndSettle();
}
// 3. Definisikan Verifikasi Asersi (Assertions)
void verifyErrorAlertIsVisible() {
expect(errorMessage, findsOneWidget);
}
void verifyOnLoginPage() {
expect(submitButton, findsOneWidget);
}
}
// integration_test/pages/home_page.dart
import 'package:patrol/patrol.dart';
import 'package:flutter_test/flutter_test.dart';
class HomePage {
final PatrolTester $;
HomePage(this.$);
PatrolFinder get welcomeBanner => $('Selamat Datang');
void verifyOnHomePage() {
expect(welcomeBanner, findsOneWidget);
}
}
Sekarang, perhatikan betapa bersih, terstruktur, dan mudah dibacanya berkas tes integrasi kita jika menggunakan pola POM:
// integration_test/flows/auth/clean_login_flow_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:patrol/patrol.dart';
import 'package:flutter_app/main.dart' as app;
import '../../pages/login_page.dart';
import '../../pages/home_page.dart';
void main() {
patrolTest(
'Alur login sukses menggunakan pola Page Object Model',
($) async {
await $.pumpWidgetAndSettle(app.MyApp());
// Inisialisasi halaman-halaman
final loginPage = LoginPage($);
final homePage = HomePage($);
// Langkah Aksi (Actions)
await loginPage.enterCredentials('[email protected]', 'password123');
await loginPage.submitLoginForm();
// Langkah Verifikasi (Assertions)
homePage.verifyOnHomePage();
},
);
patrolTest(
'Alur login gagal menampilkan pesan peringatan error',
($) async {
await $.pumpWidgetAndSettle(app.MyApp());
final loginPage = LoginPage($);
await loginPage.enterCredentials('[email protected]', 'passSalah');
await loginPage.submitLoginForm();
loginPage.verifyErrorAlertIsVisible();
loginPage.verifyOnLoginPage();
},
);
}
Eksekusi Pengujian & Mode patrol develop #
Untuk menjalankan pengujian integrasi menggunakan Patrol, pastikan emulator atau perangkat fisik kita sudah terhubung ke komputer. Kita menggunakan perintah khusus dari Patrol CLI:
# Jalankan seluruh berkas pengujian integrasi
patrol test
# Jalankan satu berkas pengujian integrasi tertentu
patrol test -t integration_test/flows/auth/clean_login_flow_test.dart
Pengembangan Interaktif: patrol develop #
Menulis integration test sering kali memakan waktu lama karena proses kompilasi ulang kode native yang berat setiap kali kita mengubah kode tes. Untuk mengatasinya, Patrol menyediakan mode interaktif yang sangat canggih bernama patrol develop:
patrol develop -t integration_test/flows/auth/clean_login_flow_test.dart
Mode develop akan mengompilasi dan memasang aplikasi ke perangkat satu kali saja, kemudian membuka sesi interaktif yang mendukung fitur Hot Restart. Ketika kita mengubah kode tes di IDE kita, cukup tekan tombol Hot Restart di terminal develop, dan tes akan diulang dalam waktu beberapa detik tanpa melakukan proses kompilasi ulang dari awal. Ini adalah fitur revolusioner yang memangkas waktu pengembangan tes integrasi secara signifikan.
Integrasi CI/CD dengan GitHub Actions #
Menjalankan integration test pada pelayan integrasi kontinu (Continuous Integration / CI) seperti GitHub Actions memerlukan perhatian khusus. Kita membutuhkan emulator Android virtual yang berjalan di server tanpa tampilan antarmuka fisik (headless mode), serta akselerasi perangkat keras KVM (Kernel-based Virtual Machine) agar emulator berjalan dengan cepat dan tidak memicu timeout.
Berikut adalah templat berkas alur kerja GitHub Actions (.github/workflows/integration_tests.yml) untuk menjalankan tes integrasi Patrol:
name: Patrol Integration Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
android-integration-tests:
# Kita menggunakan server Ubuntu karena mendukung akselerasi KVM secara penuh
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Setup Java Environment
uses: actions/setup-java@v3
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
channel: 'stable'
- name: Install Patrol CLI
run: dart pub global activate patrol_cli
- name: Aktifkan Akselerasi KVM Hardware
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: Jalankan Emulator Android & Eksekusi Patrol Test
# Pustaka runner emulator resmi untuk GitHub Actions
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 33
arch: x86_64
profile: pixel_6
disable-animations: true
# Jalankan perintah patrol test di dalam instrumen emulator
script: patrol test -t integration_test/flows/auth/clean_login_flow_test.dart
Ringkasan #
- Aplikasi Nyata: Integration test mengeksekusi aplikasi asli kita di dalam perangkat fisik atau emulator riil dengan memanggil database dan API server backend sesungguhnya.
- Kelebihan Patrol: Patrol mengatasi batas kritis
integration_testbawaan Flutter dengan mendukung kontrol elemen native OS (seperti dialog izin GPS, WebViews, dan Push Notifications).- Arsitektur Multi-Runner: Patrol menghubungkan Dart Runner (Flutter VM) dengan UI Test Runner native (UIAutomator/XCTest) menggunakan jembatan server HTTP lokal secara real-time.
- Page Object Model (POM): Pisahkan Finder dan Aksi halaman ke dalam kelas khusus untuk menghindari kerusakan penulisan tes massal saat terjadi perubahan desain antarmuka.
- Mode Interaktif: Manfaatkan perintah
patrol developsaat memprogram kode tes untuk menikmati fitur Hot Restart instan guna memangkas waktu kompilasi biner native.- CI/CD Pipeline: Jalankan tes integrasi di server Ubuntu pada alur kerja GitHub Actions dengan memanfaatkan akselerasi hardware KVM emulator untuk performa optimal.