Belajar memastikan aplikasi Flutter kamu bekerja dengan benar secara otomatis β dari menguji satu fungsi kecil sampai seluruh alur aplikasi, lalu menjalankannya otomatis lewat pipeline CI/CD.
Klik kartu mana pun untuk langsung melompat ke topiknya.
Sebelum mobil jalan, mekanik mengecek tiap komponen satu per satu β rem, ban, oli. Kalau ada yang rusak, ketahuan dari awal. Unit test menguji tiap fungsi kecil di kodemu seperti itu: terpisah, cepat, dan teliti.
Unit test memastikan logika berjalan benar tanpa melibatkan UI, jaringan, atau database.
package:test / flutter_testclass Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
Unit test Β· counter_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/counter.dart';
void main() {
test('value bertambah saat increment', () {
// Arrange
final counter = Counter();
// Act
counter.increment();
// Assert
expect(counter.value, 1);
});
}
expect membandingkan hasil aktual dengan yang diharapkan. Jika cocok β hijau β
. Jika tidak β merah β dan test gagal.
equals(1) Β· nilai samaisTrue / isNullthrowsExceptionPetugas Quality Control menekan setiap tombol remote untuk memastikan responsnya benar sebelum dikemas. Widget test menekan tombol & mengecek apakah teks di layar berubah seperti seharusnya.
testWidgets(...) β bungkus test UItester.pumpWidget(...) β render widgetfind.text('...') β cari elementester.tap(...) β simulasi taptester.pump() β refresh frameWidget test = lebih dari unit test, lebih cepat dari integration test.
testWidgets('counter naik saat tombol + ditekan',
(WidgetTester tester) async {
// 1. Render widget
await tester.pumpWidget(const MyApp());
// 2. Pastikan awalnya angka 0
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// 3. Tap tombol +
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// 4. Angka berubah jadi 1
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
Matcher khusus widget:
findsOneWidget β tepat 1findsNothing β tidak adafindsNWidgets(3) β tepat 3findsWidgets β 1 atau lebihDi pabrik, produk berpindah dari satu mesin ke mesin lain dalam satu jalur. Kalau satu mesin macet, seluruh jalur berhenti. Integration test mengecek apakah semua bagian aplikasi bekerja sama dari awal sampai akhir.
integration_testPiramida testing: banyak Unit, sedang Widget, sedikit Integration.
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:myapp/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('alur login penuh', (tester) async {
app.main();
await tester.pumpAndSettle();
await tester.enterText(find.byKey(Key('email')), 'siswa@smk.id');
await tester.enterText(find.byKey(Key('pass')), '123456');
await tester.tap(find.text('Login'));
await tester.pumpAndSettle();
expect(find.text('Selamat Datang'), findsOneWidget);
});
}
Menunggu semua animasi & proses async selesai (misalnya transisi halaman) sebelum melanjutkan pengecekan. Penting untuk alur yang nyata.
Di film aksi, aktor pakai pistol mainan, bukan asli β lebih aman dan terkontrol. Mock adalah "pemeran pengganti" untuk API/database: kita atur jawabannya sendiri, jadi test cepat dan tidak tergantung internet.
Paket populer: mockito & mocktail.
import 'package:mocktail/mocktail.dart';
import 'package:flutter_test/flutter_test.dart';
class MockApiClient extends Mock implements ApiClient {}
void main() {
test('login berhasil mengembalikan User', () async {
final api = MockApiClient();
final service = AuthService(api);
// Atur jawaban palsu
when(() => api.login('a@b.id', '123'))
.thenAnswer((_) async => User(name: 'Sahil'));
final user = await service.login('a@b.id', '123');
expect(user.name, 'Sahil');
verify(() => api.login('a@b.id', '123')).called(1);
});
}
when() β atur perilakuthenAnswer / thenReturn β hasil palsuverify() β pastikan dipanggilInspektur menandai area yang sudah ia periksa di peta. Area yang masih gelap = belum dicek. Coverage menunjukkan baris kode mana yang sudah "dikunjungi" oleh test-mu.
flutter test --coverage
Hasilnya tersimpan di coverage/lcov.info. Ubah jadi HTML:
genhtml coverage/lcov.info \
-o coverage/html
β οΈ Coverage 100% bukan berarti bebas bug β kualitas test lebih penting dari sekadar angka.
Banyak tim menargetkan 70β85% coverage pada logika inti. Itu sudah sangat sehat untuk proyek belajar maupun produksi.
Begitu barang diletakkan, ban berjalan membawanya melewati tiap stasiun secara otomatis. Sama seperti workflow: begitu kamu push kode, test langsung jalan sendiri tanpa perlu diingatkan.
Trigger β Build β Test β Report β (jika lolos) Deploy. Inilah inti otomasi: manusia menulis kode, mesin yang menguji.
Workflow hanyalah resep langkah-langkah yang ditulis dalam file teks (biasanya YAML). Mesin membaca resep itu dan menjalankannya persis urutannya.
Koper diletakkan di ban berjalan, otomatis di-scan keamanan, lalu dikirim ke pesawat. CI/CD = jalur otomatis kode: diintegrasi, diuji, lalu dirilis tanpa proses manual yang ribet.
Tools: GitHub Actions, Codemagic, GitLab CI, Bitrise.
name: Flutter CI
on:
push:
branches: [ main ]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.x'
- run: flutter pub get
- run: flutter analyze
- run: flutter test --coverage
on: β kapan dijalankanjobs: β pekerjaansteps: β langkah berurutanrun: β perintah terminalKita akan menambahkan test ke aplikasi To-Do List sederhana yang sudah ada.
dev_dependencies:
flutter_test:
sdk: flutter
mocktail: ^1.0.0
integration_test:
sdk: fluttermyapp/
ββ lib/
ββ test/ # unit & widget test
ββ integration_test/ # end-to-end testFungsi addTodo(), removeTodo(), tampilan list, dan alur menambah item baru.
test('addTodo menambah item ke list', () {
final list = TodoList();
list.add('Belajar testing');
expect(list.items.length, 1);
expect(list.items.first.title, 'Belajar testing');
});
test('removeTodo menghapus item', () {
final list = TodoList()..add('A')..add('B');
list.removeAt(0);
expect(list.items.length, 1);
expect(list.items.first.title, 'B');
});
testWidgets('menambah todo lewat UI', (tester) async {
await tester.pumpWidget(const TodoApp());
await tester.enterText(
find.byType(TextField), 'Push code');
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('Push code'), findsOneWidget);
});
flutter test --coverageflutter test integration_test/Diberikan sebuah class Kalkulator berikut:
class Kalkulator {
int tambah(int a, int b) => a + b;
int kurang(int a, int b) => a - b;
double bagi(int a, int b) {
if (b == 0) throw ArgumentError('Tidak bisa bagi 0');
return a / b;
}
}
tambah(2, 3) menghasilkan 5.kurang(10, 4) menghasilkan 6.bagi(5, 0) melempar ArgumentError (gunakan throwsArgumentError).test() dengan pola ArrangeβActβAssert, semua hijau saat flutter test.Hint: untuk soal 3, gunakan expect(() => kalk.bagi(5,0), throwsArgumentError);
Sebuah halaman ProfilePage mengambil nama user dari UserRepository lalu menampilkannya di sebuah Text.
class ProfilePage extends StatelessWidget {
final UserRepository repo;
const ProfilePage(this.repo, {super.key});
// memanggil repo.getName() lalu menampilkannya
}
MockUserRepository menggunakan mocktail.when(() => mock.getName()).thenReturn('Sahil').ProfilePage(mock) dengan pumpWidget.find.text('Sahil') menghasilkan findsOneWidget.Hint: bungkus widget dengan MaterialApp(home: ...) agar tidak error layout saat di-render.
Pilih satu aplikasi Flutter yang sudah kamu buat di BAB sebelumnya (mis. app login dari BAB 3 atau app API dari BAB 1).
integration_test ke project.pumpAndSettle() untuk menunggu transisi..github/workflows/flutter.yml agar test jalan otomatis saat push.Selamat! Setelah ini kamu sudah menguasai seluruh piramida testing Flutter: Unit β Widget β Integration β Automation. π