Flutter diingat ketika perlu untuk membuat aplikasi yang cantik dan responsif dengan cepat untuk beberapa platform sekaligus, tetapi bagaimana menjamin kualitas kode "cepat"?
Anda akan terkejut, tetapi Flutter memiliki sarana untuk tidak hanya memastikan kualitas kode, tetapi juga untuk menjamin pengoperasian antarmuka visual.
Dalam artikel ini, kita akan memeriksa bagaimana hal-hal dengan tes pada Flutter, kita akan menganalisis tes widget dan pengujian integrasi aplikasi secara keseluruhan.

Saya mulai mempelajari Flutter lebih dari setahun yang lalu, sebelum rilis resminya, selama penelitian, tidak ada masalah untuk menemukan informasi pengembangan. Dan ketika saya ingin mencoba TDD , ternyata informasi tentang pengujian sangat kecil. Di Rusia, dan secara umum, hampir tidak ada. Masalah pengujian harus dipelajari secara independen, sesuai dengan kode sumber tes Flutter dan artikel langka dalam bahasa Inggris. Semua yang saya pelajari tentang pengujian elemen visual, saya jelaskan dalam sebuah artikel untuk membantu mereka yang baru saja mulai mempelajari topik tersebut.
Informasi umum
Tes widget menguji satu widget. Ini juga bisa disebut uji komponen. Tujuan dari tes ini adalah untuk membuktikan bahwa antarmuka pengguna widget terlihat dan berinteraksi sesuai rencana. Menguji widget membutuhkan lingkungan pengujian yang menyediakan konteks yang sesuai untuk siklus hidup widget.
Widget yang diuji memiliki kemampuan untuk menerima tindakan dan peristiwa pengguna, dan meresponsnya, membangun pohon widget anak. Oleh karena itu, tes widget lebih kompleks daripada tes unit. Namun, seperti pengujian unit, lingkungan pengujian widget adalah simulasi sederhana, jauh lebih sederhana daripada sistem antarmuka pengguna lengkap.
Pengujian widget memungkinkan Anda untuk mengisolasi dan menguji perilaku elemen tunggal dari antarmuka visual. Dan, yang luar biasa, untuk melakukan semua pemeriksaan di konsol, yang ideal untuk pengujian yang dijalankan sebagai bagian dari proses CI / CD.
File yang berisi tes biasanya terletak di subdirektori tes proyek.
Tes dapat dijalankan baik dari IDE atau dari konsol dengan perintah:
$ flutter test
Dalam hal ini, semua tes dengan mask * _test.dart dari subdirektori tes akan dieksekusi.
Anda dapat menjalankan tes terpisah dengan menentukan nama file:
$ flutter test test/phone_screen_test.dart
Tes ini dibuat oleh fungsi testWidgets , yang menerima alat sebagai parameter tester , dengan mana kode tes berinteraksi dengan widget yang diuji:
testWidgets(' ', (WidgetTester tester) async {
Untuk menggabungkan tes ke dalam blok logis, fungsi tes dapat digabungkan ke dalam grup, di dalam fungsi grup :
group(' ', (){ testWidgets(' ', (WidgetTester tester) async {
Fungsi setUp dan tearDown memungkinkan Anda untuk mengeksekusi beberapa kode "sebelum" dan "setelah" setiap pengujian. Dengan demikian, fungsi setUpAll dan tearDownAll memungkinkan Anda untuk menjalankan kode "sebelum" dan "setelah" semua tes, dan jika fungsi-fungsi ini dipanggil dalam grup, mereka akan dipanggil "sebelum" dan "setelah" pelaksanaan semua tes grup:
setUp(() {
Pencarian widget
Untuk melakukan beberapa tindakan pada widget bersarang, Anda perlu menemukannya di pohon widget. Untuk melakukan ini, ada objek pencarian global yang memungkinkan Anda menemukan widget:
- di pohon dengan teks - find.text , find.widgetWithText ;
- dengan kunci - find.byKey ;
- dengan ikon - find.byIcon , find.widgetWithIcon ;
- berdasarkan tipe - find.byType ;
- oleh posisi di pohon - find.descendant dan find.ancestor ;
- menggunakan fungsi yang menganalisis widget pada daftar - find.byWidgetPredicate .
Uji interaksi widget
Kelas WidgetTester menyediakan fungsi untuk membuat widget uji, menunggu keadaannya berubah, dan untuk melakukan beberapa tindakan pada widget ini.
Setiap perubahan pada widget menyebabkan perubahan pada kondisinya. Tetapi lingkungan pengujian tidak membangun kembali widget secara bersamaan. Anda harus secara independen menunjukkan ke lingkungan pengujian bahwa Anda ingin membangun kembali widget dengan memanggil fungsi pump atau pumpAndSettle .
- pumpWidget - buat widget tes;
- pump - mulai memproses transisi status widget dan menunggu untuk menyelesaikannya dalam batas waktu yang ditentukan (100 ms secara default);
- pumpAndSettle - memanggil pump dalam siklus untuk mengubah status selama batas waktu yang diberikan (100 ms secara default), ini adalah penantian untuk semua animasi selesai;
- ketuk - kirim klik ke widget;
- longPress - tekan lama;
- fling - swipe / swipe;
- seret - transfer;
- enterText - input teks.
Tes dapat menerapkan kedua skenario positif, memeriksa peluang yang direncanakan, dan yang negatif untuk memastikan bahwa mereka tidak mengarah pada konsekuensi fatal, misalnya, ketika pengguna mengklik ke arah yang salah dan masuk bukan apa yang diperlukan:
await tester.enterText(find.byKey(Key('phoneField')), 'bla-bla-bla');
Setelah melakukan tindakan apa pun dengan widget, Anda perlu memanggil tester.pumpAndSettle () untuk mengubah status.
Moki
Banyak yang akrab dengan perpustakaan Mockito . Perpustakaan ini dari dunia Jawa ternyata sangat sukses sehingga ada implementasi perpustakaan ini dalam banyak bahasa pemrograman, termasuk Dart.
Untuk terhubung, Anda harus menambahkan ketergantungan ke proyek. Tambahkan baris berikut ke file pubspec.yaml :
dependencies: mockito: any
Dan sambungkan dalam file tes:
import 'package:mockito/mockito.dart';
Pustaka ini memungkinkan Anda untuk membuat kelas moque, yang bergantung pada widget yang diuji, sehingga pengujian lebih sederhana dan hanya mencakup kode yang kami uji.
Misalnya, jika kami menguji widget PhoneInputScreen , yang, ketika diklik, menggunakan layanan AuthInteractor , melakukan permintaan ke backend authInteractor.checkAccess () , lalu mengganti tiruan alih-alih layanan, kami dapat memeriksa hal yang paling penting - fakta mengakses layanan ini.
Dependency mobs dibuat sebagai turunan dari kelas Mock dan mengimplementasikan antarmuka ketergantungan:
class AuthInteractorMock extends Mock implements AuthInteractor {}
Kelas di Dart juga merupakan antarmuka, sehingga tidak perlu mendeklarasikan antarmuka secara terpisah, seperti pada beberapa bahasa pemrograman lainnya.
Untuk menentukan fungsionalitas mok, fungsi when digunakan, yang memungkinkan Anda untuk menentukan respons mok terhadap panggilan ke fungsi tertentu:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.value(true));
Moki dapat mengembalikan kesalahan atau data yang salah:
when( authInteractor.checkAccess(any), ).thenAnswer((_) => Future.error(UnknownHttpStatusCode(null)));
Cek
Selama pengujian, Anda dapat memeriksa widget di layar. Ini memungkinkan Anda untuk memastikan bahwa keadaan layar yang baru sudah benar dalam hal visibilitas widget yang diinginkan:
expect(find.text(' '), findsOneWidget); expect(find.text(' '), findsNothing);
Setelah tes selesai, Anda juga dapat memeriksa metode mana dari kelas mob yang dipanggil selama tes, dan berapa kali. Ini diperlukan, misalnya, untuk memahami apakah data ini atau itu terlalu sering diminta, apakah ada perubahan yang tidak perlu dalam kondisi aplikasi:
verify(appComponent.authInteractor).called(1); verify(authInteractor.checkAccess(any)).called(1); verifyNever(appComponent.profileInteractor);
Debugging
Tes dilakukan di konsol tanpa grafis. Anda dapat menjalankan tes dalam mode debug dan mengatur breakpoints dalam kode widget.
Untuk mendapatkan gambaran tentang apa yang terjadi di pohon widget, Anda dapat menggunakan fungsi debugDumpApp () , yang, ketika dipanggil dalam kode uji, menampilkan representasi tekstual dari hierarki seluruh pohon widget pada waktu tertentu di konsol.
Untuk memahami bagaimana widget menggunakan moki ada fungsi logInvocations () . Dibutuhkan sebagai parameter daftar moxas dan masalah ke konsol urutan metode panggilan untuk moxas ini yang dilakukan dalam pengujian.
Contoh kesimpulan seperti itu di bawah ini. Tanda VERIFIED adalah pada panggilan yang diperiksa dalam pengujian menggunakan fungsi verifikasi :
AppComponentMock.sessionChangedInteractor [VERIFIED] AppComponentMock.authInteractor [VERIFIED] AuthInteractorMock.checkAccess(71111111111)
Persiapan
Semua dependensi harus diserahkan ke widget yang diuji dalam bentuk mok:
class SomeComponentMock extends Mock implements SomeComponent {} class AuthInteractorMock extends Mock implements AuthInteractor {}
Pemindahan dependensi ke komponen yang diuji harus dilakukan dengan cara tertentu yang diterima dalam aplikasi Anda. Untuk kesederhanaan bercerita, pertimbangkan contoh di mana dependensi dilewatkan melalui konstruktor.
Dalam contoh kode, PhoneInputScreen adalah widget uji berdasarkan StatefulWidget dibungkus dalam Scaffold . Itu dibuat di lingkungan pengujian menggunakan fungsi pumpWidget () :
await tester.pumpWidget(PhoneInputScreen(mock));
Namun, widget nyata dapat menggunakan penyelarasan untuk widget bersarang, yang membutuhkan MediaQuery di pohon widget, widget mungkin mendapatkan Navigator.of (konteks) untuk navigasi, jadi lebih praktis untuk membungkus widget yang sedang diuji di MaterialApp atau CupertinoApp :
await tester.pumpWidget( MaterialApp( home: PhoneInputScreen(mock), ), );
Setelah membuat widget tes dan setelah tindakan apa pun, Anda perlu memanggil tester.pumpAndSettle () sehingga lingkungan pengujian menangani semua perubahan dalam keadaan widget.
Tes integrasi
Informasi umum
Tidak seperti tes widget, tes integrasi memeriksa seluruh aplikasi atau sebagian besar dari itu. Tujuan dari tes integrasi adalah untuk memastikan bahwa semua widget dan layanan bekerja sama seperti yang diharapkan. Pengoperasian tes integrasi dapat diamati di simulator atau di layar perangkat. Metode ini merupakan pengganti yang baik untuk pengujian manual. Selain itu, tes integrasi dapat digunakan untuk menguji kinerja aplikasi.
Tes integrasi biasanya dilakukan pada perangkat nyata atau emulator, seperti iOS Simulator atau Android Emulator.
File yang berisi tes integrasi biasanya terletak di subdirektori test_driver proyek.
Aplikasi diisolasi dari kode driver tes dan mulai setelah itu. Driver tes memungkinkan Anda untuk mengontrol aplikasi selama pengujian. Ini terlihat seperti ini:
import 'package:flutter_driver/driver_extension.dart'; import 'package:app_package_name/main.dart' as app; void main() { enableFlutterDriverExtension(); app.main(); }
Tes dijalankan dari baris perintah. Jika peluncuran aplikasi target dijelaskan dalam file app.dart , dan skrip pengujian disebut app_test.dart , maka perintah berikut sudah cukup:
$ flutter drive --target=test_driver/app.dart
Jika skrip uji memiliki nama yang berbeda, maka Anda harus menentukannya secara eksplisit:
$ flutter drive --target=test_driver/app.dart --driver=test_driver/home_test.dart
Tes dibuat oleh fungsi tes , dan dikelompokkan berdasarkan fungsi grup .
group('park-flutter app', () {
Contoh ini menunjukkan kode untuk membuat driver tes di mana tes berinteraksi dengan aplikasi yang diuji.
Interaksi dengan aplikasi yang diuji
Alat FlutterDriver berinteraksi dengan aplikasi pengujian melalui metode berikut:
- ketuk - kirim klik ke widget;
- waitFor - tunggu widget muncul di layar;
- waitForAbsent - tunggu widget menghilang;
- gulir dan gulirIntoView , scrollUntilVisible - gulir layar ke offset yang ditentukan atau ke widget yang diinginkan;
- enterText , getText - masukkan teks atau ambil teks widget;
- tangkapan layar - dapatkan tangkapan layar;
- requestData - interaksi yang lebih kompleks melalui pemanggilan fungsi di dalam aplikasi yang sedang diuji.
Mungkin ada situasi ketika Anda perlu memengaruhi status aplikasi global dari kode tes. Misalnya, untuk menyederhanakan uji integrasi dengan mengganti bagian dari layanan dalam aplikasi dengan moki. Dalam aplikasi, Anda dapat menentukan penangan permintaan, yang dapat diakses melalui panggilan ke driver.requestData ('some param') dalam kode tes:
void main() { Future<String> dataHandler(String msg) async { if (msg == "some param") {
Pencarian widget
Pencarian widget selama pengujian integrasi dengan objek penemuan global berbeda dalam komposisi metode dari fungsionalitas yang sama dalam pengujian widget. Namun, makna umum secara praktis tidak berubah:
- di pohon dengan teks - find.text , find.widgetWithText ;
- dengan kunci - find.byValueKey ;
- berdasarkan tipe - find.byType ;
- di prompt - find.byTooltip ;
- oleh label semantik - find.bySemanticsLabel ;
- oleh posisi di pohon find.descendant dan find.ancestor .
Kesimpulan
Kami mencari cara untuk mengatur pengujian antarmuka aplikasi yang ditulis menggunakan Flutter. Kami berdua dapat mengimplementasikan tes untuk memverifikasi bahwa kode memenuhi persyaratan spesifikasi teknis, dan melakukan tes dengan tugas ini. Dari kekurangan pengujian integrasi yang dicatat - tidak ada cara untuk berinteraksi dengan dialog sistem platform. Tetapi, misalnya, permintaan izin dapat dihindari dengan mengeluarkan izin dari baris perintah pada tahap instalasi aplikasi, seperti yang dijelaskan dalam tiket ini .
Artikel ini adalah titik awal untuk mengeksplorasi topik pengujian yang secara singkat memperkenalkan pembaca tentang cara kerja pengujian antarmuka pengguna. Itu tidak menyimpan dokumentasi bacaan, dari mana itu cukup mudah untuk mengetahui bagaimana fungsi kelas atau metode tertentu. Lagi pula, mempelajari topik baru untuk Anda sendiri membutuhkan, pertama-tama, pemahaman tentang semua proses yang sedang berlangsung secara keseluruhan, tanpa detail yang berlebihan.