Aplikasi yang diperbarui terus-menerus membutuhkan cara yang murah untuk memastikan tidak ada perubahan perilaku yang terjadi tanpa sengaja. Topik ini membahas tiga bentuk pengujian pada Flutter, yaitu tes unit untuk logika murni, tes widget untuk antarmuka, dan golden test untuk perbandingan tampilan. Pembahasan ditutup dengan kebiasaan penulisan kode yang membuat pengujian menjadi mudah. Setelah topik ini dibaca, setiap bagian aplikasi catatan dapat diuji pada tingkat yang sesuai dengan biayanya.
Tiga tingkat pengujian#
Flutter membagi pengujian menjadi beberapa tingkat menurut cakupannya. Tabel berikut merangkum tiga tingkat yang dibahas oleh dokumentasi resmi beserta karakternya.
| Tingkat | Yang diuji | Kecepatan |
|---|---|---|
| Tes unit | fungsi dan kelas logika murni | paling cepat, tanpa UI |
| Tes widget | satu widget beserta interaksinya | cepat, UI dirender di memori |
| Integration test | aplikasi utuh pada emulator atau perangkat | paling lambat |
Semakin tinggi tingkatnya, semakin mahal biaya penulisannya dan semakin lambat dijalankannya, sehingga komposisi yang sehat condong ke tes unit dan tes widget. Integration test menyempurnakan keduanya dengan menguji alur nyata pada aplikasi utuh yang berjalan, dan pembahasannya cukup pada pengenalan ini karena memerlukan emulator.
Tes unit#
Tes unit menguji logika tanpa menggambar apa pun, sehingga penulisannya paling murah dan eksekusinya paling cepat. Sasaran yang tepat adalah fungsi murni, aturan validasi, penghitungan, dan pengubahan data. Kelas berikut memisahkan aturan validasi judul catatan dari widget mana pun, dan kelas semacam inilah yang paling mudah diuji.
// tidak-dijalankan
class ValidasiCatatan {
static String? judul(String nilai) {
final bersih = nilai.trim();
if (bersih.isEmpty) {
return 'Judul wajib terisi';
}
if (bersih.length > 100) {
return 'Judul maksimal 100 karakter';
}
return null;
}
}Metode judul mengembalikan String? berisi pesan error atau null ketika nilainya sah, sehingga setiap aturannya dapat diuji satu per satu. Berkas pengujinya ditulis di direktori test dengan nama yang diakhiri _test.dart. Berkas berikut menguji kelas validasi di atas sekaligus satu widget layar catatan yang menerima data melalui konstruktor.
// tidak-dijalankan
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:aplikasi_catatan/layar_catatan.dart';
import 'package:aplikasi_catatan/validasi_catatan.dart';
void main() {
group('ValidasiCatatan.judul', () {
test('menolak judul yang kosong', () {
expect(ValidasiCatatan.judul(' '), isNotNull);
});
test('menerima judul yang berisi teks', () {
expect(ValidasiCatatan.judul('Belanja mingguan'), isNull);
});
test('menolak judul lebih dari 100 karakter', () {
final terlaluPanjang = 'a' * 101;
expect(ValidasiCatatan.judul(terlaluPanjang), isNotNull);
});
});
group('LayarCatatan', () {
testWidgets('menampilkan seluruh catatan yang diterima', (tester) async {
await tester.pumpWidget(const MaterialApp(
home: LayarCatatan(catatan: ['Belanja mingguan', 'Isi ulang galon']),
));
expect(find.text('Belanja mingguan'), findsOneWidget);
expect(find.text('Isi ulang galon'), findsOneWidget);
});
testWidgets('menampilkan pesan kosong saat daftar tanpa isi', (tester) async {
await tester.pumpWidget(const MaterialApp(
home: LayarCatatan(catatan: []),
));
expect(find.text('Belum ada catatan'), findsOneWidget);
});
});
}Seluruh pengujian dijalankan dengan satu perintah.
# tidak-dijalankan
# menjalankan semua tes pada proyek
flutter testPerintah tersebut mencari setiap berkas berakhiran _test.dart di direktori test, menjalankannya tanpa emulator, lalu melaporkan jumlah yang lulus. Hasil yang lulus atau gagal beserta pesan errornya ditampilkan pada terminal, dan prosesnya selesai dalam hitungan detik untuk proyek berukuran kecil.
Tes widget dengan WidgetTester#
Tes widget menjalankan widget sungguhan di dalam memori tanpa perangkat. Fungsi testWidgets menerima callback dengan objek WidgetTester yang bertugas merender dan memanipulasi pohon widget. Tiga peralatan utamanya adalah pumpWidget untuk merender pohon dari awal, find untuk mencari widget berdasarkan teks atau tipenya, dan expect untuk membandingkan temuannya dengan harapan.
Peran pumpWidget dan pump perlu dibedakan dengan cermat. pumpWidget merender pohon sekali, sedangkan pump melanjutkan ke bingkai (frame) berikutnya. Setiap interaksi atau setState yang mengubah tampilan perlu diikuti pump sebelum asersi, karena perubahan baru terlihat setelah bingkai selesai digambar.
// tidak-dijalankan
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
expect(find.text('1'), findsOneWidget);Untuk perubahan yang menyertakan animasi, pumpAndSettle menunggu hingga seluruh animasi berhenti sebelum asersi dijalankan. Dua contoh tes widget pada berkas catatan_test.dart di atas menggunakan pola paling dasar, yaitu pumpWidget sekali diikuti pemeriksaan teks yang muncul di layar.
Golden test sebagai pengenalan#
Golden test membandingkan gambar hasil render widget dengan berkas acuan (golden file) yang tersimpan di dalam proyek. Pengujian lulus apabila piksel yang dirender sama dengan acuannya, dan gagal apabila ada perbedaan sekecil apa pun. Bentuknya berguna untuk menjaga tampilan komponen penting tetap stabil antarrevisi, misalnya kartu yang menampilkan jadwal piket.
// tidak-dijalankan
testWidgets('kartu catatan dirender sesuai acuan', (tester) async {
await tester.pumpWidget(const MaterialApp(
home: KartuCatatan(judul: 'Jadwal piket'),
));
await expectLater(
find.byType(KartuCatatan),
matchesGoldenFile('goldens/kartu_catatan.png'),
);
});Berkas acuan dibuat dengan opsi --update-goldens pada flutter test ketika perubahan tampilan memang disengaja, sehingga asersi berikutnya membandingkan diri dengan tampilan baru tersebut. Satu sifat yang perlu diketahui sejak awal adalah hasil render dapat berbeda antarplatform karena perbedaan perenderan teks dan font, sehingga proyek yang menuntut tampilan konsisten memisahkan acuan per platform. Untuk tahap awal, yang penting dikenali adalah perannya, yaitu menutup celah yang tidak terjangkau asersi teks.
Kebiasaan yang membuat kode mudah dites#
Kesulitan menguji hampir selalu berasal dari kode, bukan dari kerangka pengujinya. Widget yang mengambil data sendiri, misalnya memanggil basis data langsung di dalam metode build, memaksa pengujian menyiapkan data nyata yang lambat dan tidak pasti. Kebiasaan yang menghindari hal itu ada tiga.
Pertama, terima data dan layanan melalui konstruktor, sebagaimana LayarCatatan pada berkas pengujian yang menerima daftar catatan dari luar. Kedua, pisahkan logika dari widget, seperti kelas ValidasiCatatan yang aturannya dapat diuji tanpa merender apa pun. Ketiga, terapkan injeksi dependensi sederhana, yaitu pendekatan yang membuat layanan seperti pembaca waktu atau penyimpanan diterima sebagai parameter agar dapat diganti dengan objek palsu (fake) saat pengujian. Ketiganya membuat pengujian tidak perlu menunggu jaringan, emulator, atau data nyata.
Kesalahan umum#
- Memanggil
expecttanpapumpsetelah interaksi. Tampilan belum dirender pada bingkai berikutnya, sehingga asersi gagal padahal kodenya benar. Tambahkanawait tester.pump()sebelum memeriksa. - Menguji detail internal alih-alih perilaku yang terlihat oleh pengguna. Asersi terhadap tipe internal rapuh terhadap penyusunan ulang widget, sedangkan asersi terhadap teks dan keadaan layar lebih tahan perubahan.
- Menulis widget yang mengambil data sendiri. Pengujian menjadi bergantung pada sumber data nyata. Pindahkan pemberian data ke konstruktor.
- Memperbarui berkas acuan golden tanpa disengaja. Acuan yang diperbarui sekadar agar tes lulus menyembunyikan perubahan tampilan yang seharusnya diperiksa.
Ringkasan#
- Tes unit menguji logika murni dan paling murah, tes widget menguji antarmuka dengan
WidgetTester, sedangkan integration test menguji aplikasi utuh pada emulator. pumpWidgetmerender pohon sekali,pumpmelanjutkan bingkai berikutnya, dan asersi setelah interaksi wajib menunggupump.- Golden test membandingkan piksel render dengan berkas acuan yang diperbarui secara sengaja melalui opsi
--update-goldens. - Kode yang mudah dites menerima data melalui konstruktor, memisahkan logika dari widget, dan menerapkan injeksi dependensi sederhana.
Referensi#
- Testing Flutter apps — Google — Peta resmi kategori pengujian Flutter beserta perkakasnya.
- An introduction to unit testing — Google — Panduan resmi menulis dan menjalankan tes unit.
- An introduction to widget testing — Google — Panduan resmi tes widget dengan WidgetTester serta finder.