Tingkatkan Kualitas Aplikasi Flutter dengan Menerapkan Proses Test-Driven Development (TDD) dan Mengadopsi Clean Architecture
Konsep detail dan implementasi TDD dan Clean Architecture pada Flutter
Sejak dirilis ke publik pada 4 Desember 2018, ekosistem pengembangan aplikasi dengan Flutter sudah mengalami perkembangan yang sangat pesat. Memilih Flutter sebagai SDK untuk mengembangkan produk dengan iterasi yang cepat adalah pilihan yang tepat. Namun, bagaimana pun ketika memilih Flutter, developer juga harus mulai memikirkan state management pada aplikasi dan bagaimana merancang aplikasi (di balik layar) agar mudah diperihara dan scalable.
Untuk menjawab keresahan tersebut, kita perlu merancang arsitektur aplikasi sebelum mulai mengembangkan fitur.
Clean Architecture
Arsitektur adalah hal yang penting dalam pengembangan sebuah aplikasi. Arsitektur dapat diibaratkan seperti sebuah denah yang menggambarkan bagaimana alur dalam sebuah project aplikasi. Fungsi dari menerapkan arsitektur yang paling dasar adalah separation of concern (SoC). Jadi, akan lebih mudah jika kita dapat bekerja dengan fokus pada satu hal dalam satu waktu.
Dalam konteks Flutter, clean architecture akan membantu kita untuk memisahkan kode untuk business-logic dengan kode yang berhubungan dengan platform seperti UI, state management, dan sumber data eksternal. Selain itu, kode yang kita tulis pun dapat lebih mudah untuk diuji (testable) secara independen.
Seperti pada diagram di atas, clean architecture digambarkan seperti sebuah piramida atau irisan bawang jika dilihat dari sisi atas. Clean architecture membagi project ke dalam 3 lapisan (layer) yaitu:
Lapisan data & platform
Lapisan data terletak pada lapisan paling luar. Lapisan ini terdiri dari kode sumber data seperti Rest API, local database, Firebase, atau sumber lainnnya. Juga kode platform yang membangun tampilan aplikasi (widgets).
Lapisan presentation
Lapisan presentasi terdiri dari kode yang menjembatani komunikasi antara data dengan tampilan aplikasi yang disebut repository. Pada lapisan ini juga kita dapat meletakkan kode untuk state management aplikasi seperti provider, controller, BLoC, dan lain sebagainya.
Lapisan domain
Lapisan domain adalah lapisan terdalam pada clean architecture. Pada lapisan ini terdapat kode untuk business-logic aplikasi seperti entities dan usecases.
Masing-masing lapisan bergantung (depends) pada lapisan lainnya. Anak panah pada diagram menunjukkan bagaimana hubungan antar lapisan. Lapisan paling luar akan bergantung pada lapisan bagian dalam dan seterusnya.
Lapisan yang tidak bergantung pada lapisan lain di sini hanya lapisan domain (independen) yang merupakan kode untuk business-logic. Dengan begitu, aplikasi lebih bisa beradaptasi dan dinamis. Misalnya kita ingin mengganti state management dari provider menjadi BLoC, maka proses migrasi tidak akan mengganggu business-logic yang sudah ada.
Test-Driven Development
Selain menerapkan clean architecture, untuk mengoptimalkan proses pengembangan dalam arti menghasilkan aplikasi yang minim bugs dan mengurangi proses debugging dan fixing yang repetitif, kita harus menjalani proses testing.
Test-Driven Development adalah sebuah proses pengembangan aplikasi dimana testing yang mengatur jalannya pengembangan. Skenario kode testing akan ditulis terlebih dahulu sebelum membuat sebuah fitur pada aplikasi.
Alur kerja dari proses pengembangan aplikasi dengan TDD seperti diagram diatas. Proses TDD adalah proses yang berulang.
Step — 1: Menulis skenario testing
Pengembangan sebuah fitur dimulai dengan menulis skenario testing terlebih dahulu. Penulisan skenario testing biasanya mengikuti feature requirements pada sebuah dokumen PRD (untuk kasus di sebuah perusahaan). Pada skenario pengujian biasanya akan terdapat alur dari fitur yang dikembangkan seperti menentukan sumber data yang digunakan (misalnya remote atau local), memastikan data yang masuk dari API menghasilkan model yang sesuai, merancang alur state dari tampilan berdasarkan data yang masuk ke aplikasi, dan lain sebagainya. Pada saat pertama kali menulis skenario testing, kita pasti akan mendapatkan error. Hal tersebut normal karena kode fiturnya belum ada.
Menulis skenario testing sebuah fitur dilakukan sebagai panduan dalam mengembangkan fitur (actual-code).
Step — 2: Menulis kode fitur untuk membuat kode testing berhasil
Pada langkah ini, penulisan kode fitur yang sebenarnya dilakukan. Penulisan kode pada tahap ini tidak harus rapi dan optimal karena tujuan utama pada langkah ini adalah membuat kode testing yang sudah dibuat sebelumnya menjadi berhasil.
Step — 3: Refactor
Setelah kode testing berhasil dijalankan tanpa adanya error, sekarang saatnya untuk merapikan dan mengoptimalkan kode yang sudah ditulis baik itu kode testing maupun kode fitur.
Dengan proses TDD, hasil akhir yang akan didapat selain aplikasi yang minim bugs, juga kode aplikasi yang rapi dan optimal.
Implementasi
Sekarang, kita akan coba mengimplementasikan konsep Test-Driven Development dan clean architecture dengan sebuah mini-project yaitu aplikasi cuaca. Kali ini kita akan memanfaatkan OpenWeather API sebagai sumber data dari aplikasi kita.
Struktur folder project ini adalah sebagai berikut:
lib
├── data
│ ├── constants.dart
│ ├── datasources
│ │ └── remote_data_source.dart
│ ├── exception.dart
│ ├── failure.dart
│ ├── models
│ │ └── weather_model.dart
│ └── repositories
│ └── weather_repository_impl.dart
├── domain
│ ├── entities
│ │ └── weather.dart
│ ├── repositories
│ │ └── weather_repository.dart
│ └── usecases
│ └── get_current_weather.dart
├── injection.dart
├── main.dart
└── presentation
├── bloc
│ ├── weather_bloc.dart
│ ├── weather_event.dart
│ └── weather_state.dart
└── pages
└── weather_page.darttest
├── data
│ ├── datasources
│ │ └── remote_data_source_test.dart
│ ├── models
│ │ └── weather_model_test.dart
│ └── repositories
│ └── weather_repository_impl_test.dart
├── domain
│ └── usecases
│ └── get_current_weather_test.dart
├── helpers
│ ├── dummy_data
│ │ └── dummy_weather_response.json
│ ├── json_reader.dart
│ ├── test_helper.dart
│ └── test_helper.mocks.dart
└── presentation
├── bloc
│ └── weather_bloc_test.dart
└── pages
└── weather_page_test.dart
Jika kamu ingin mengikuti tutorial ini, kamu dapat menggunakan starter code di github repository berikut:
Step — 1: Membuat kode pada lapisan domain
Langkah pertama adalah membuat kode pada lapisan domain. Kenapa lapisan domain? karena lapisan ini adalah lapisan yang tidak bergantung kepada lapisan apapun. Jadi akan lebih aman jika dimulai dari lapisan ini.
Menulis kode testing dan fitur usecase
Pada bagian test/domain, kita hanya perlu menuliskan skenario testing untuk usecases. Pada kasus ini kita memiliki 1 buah usecase yaitu get_current_weather_test.dart
.
Kode testing usecase diatas akan error diawal. Hal tersebut normal karena kita belum menulis kode fiturnya.
Untuk kode fitur pada lapisan domain, terdapat 3 bagian yaitu entities, usecases, dan repositories. Kita mulai dengan menulis weather entity yaitu weather.dart
.
Setelah itu dilanjutkan dengan membuat kode untuk weather_repository.dart
. Weather repository merupakan sebuah abstract class dan akan diimplementasikan nanti pada lapisan data.
Kemudian, kita akan membuat kode untuk usecase get_current_weather.dart
.
Sebelum kembali ke kode testing, kita harus membuat mock weather repository. Buka test_helper.dart
lalu tambahkan WeatherRepository
. Pada test_helper.dart
terdapat juga mock untuk library HTTP agar dapat melakukan simulasi request ke API pada saat testing.
Jalankan command berikut melalui terminal untuk menghasilkan mock file:
flutter pub run build_runner build
Setelah itu, kembali ke kode get_current_weather.dart
dan import MockWeatherRepository
lalu jalankan testing.
Step — 2: Membuat kode pada lapisan data
Setelah mengerjakan kode testing dan fitur pada lapisan domain, sekarang kita lanjut untuk mengerjakan kode pada lapisan data. Pada lapisan data, terdapat data souces, models, dan repository.
Sebentar, di lapisan domain juga ada repository? Iya, repository pada lapisan domain berbentuk abstract class yang didalamnya terdapat fungsi-fugsi yang masih harus perlu diimplementasikan. Nah, di repository pada lapisan data lah kita akan mulai mengimplementasikan abstract class tersebut.
Menulis kode testing dan fitur models
Oke, kita akan mulai dari models, proses diawali dengan menulis kode testing untuk model yaitu weather_model_test.dart
. Di sini, kita akan menguji 3 hal yaitu:
- Apakah model yang kita buat sudah sesuai dengan entity pada lapisan domain?
- Apakah fungsi
fromJson()
mengembalikan model yang valid? - Apakah fungsi
toJson()
mengembalikan JSON map yang sesuai?
Kemudian, kita lanjutkan dengan menulis kode untuk weather_model.dart
.
Setelah itu, kembali lagi ke kode testing, import weather_model.dart
lalu jalankan testing.
Menulis kode testing dan fitur data sources
Kita lanjutkan dengan mengerjakan kode pada data sources. Berikut adalah kode untuk remote_data_source_test.dart
.
Kode remote_data_source_test.dart
di atas akan menguji proses mendapatkan data cuaca dari API. Ada dua kondisi di sini yaitu (1) mengembalikan model yang valid ketika membaca data berhasil, dan (2) mengembalikan exception ketika membaca data gagal. Kita juga membutuhkan dummy data dalam bentuk JSON seperti ini:
Kemudian lanjut dengan menuliskan kode untuk data source yaitu remote_data_source.dart
.
Setelah itu, kembali lagi ke kode testing dan import remote_data_source.dart
lalu jalankan testing.
Menulis kode testing dan fitur repository
Selanjutnya, kita akan mengerjakan kode untuk repository yaitu weather_repository_impl_test.dart
dan weather_repository_impl.dart
. Istilah impl
di sini berarti implementation.
Untuk testing pada weather_repository_impl_test.dart
kita butuh untuk mengakses data source. Jadi, kita akan membuat mock dari RemoteDataSource
yang sudah kita buat sebelumnya. Tambahkan RemoteDataSource
pada test_helper.dart
seperti berikut:
Kemudian, jalankan command berikut untuk menghasilkan mock file:
flutter pub run build_runner build
MockRemoteDataSource
udah siap digunakan. Sekarang kita akan mengerjakan kode untuk weather_repository_impl_test.dart
.
Pada kode diatas skenario yang diuji adalah:
- Mengembalikan current weather data saat request ke API sukses
- Mengembalikan
ServerFailure
saat request ke API gagal - Mengembalikan
ConnectionFailure
saat tidak terkoneksi ke internet
Sekarang itu kita lanjut dengan mengerjakan weather_repository_impl.dart
.
Setelah itu, kembali lagi ke kode testing dan import weather_repository_impl.dart
lalu jalankan testing.
Step — 3: Membuat kode pada lapisan presentation
Setelah mengerjakan pada lapisan domain dan data, langkah terakhir adalah mengerjakan kode pada lapisan presentation. Pada lapisan ini, terdapat state management (pada kasus ini kita menggunakan BLoC), dan pages.
Menulis kode testing dan fitur BLoC
Pertama, kita akan membuat kode state management (BLoC). Untuk testing pada weather_bloc_test.dart
, kita butuh untuk mengakses usecase yaitu GetCurrentWeather
.
Selanjutnya jalankan command berikut untuk menghasilkan mock file:
flutter pub run build_runner build
Kemudian, kita lanjut dengan mengerjakan kode state management BLoC. Di sini kode BLoC kita terdiri dari weather_state.dart
, weather_event.dart
, dan weather_bloc.dart
.
Setelah itu, kembali lagi ke kode testing dan import weather_bloc.dart
lalu jalankan testing.
Menulis kode testing dan fitur Pages
Setelah mengerjakan kode BLoC, selanjutnya kita akan membuat kode untuk pages atau UI dari aplikasi. Kita mulai dulu dengan kode testing yaitu weather_page_test.dart
. Pada pengujian ini kita akan menguji 3 hal yaitu:
- State dari aplikasi seharusnya berubah dari
WeatherEmpty
menjadiWeatherLoading
ketika selesai memasukkan data ke widgetTextField
. - Menampilkan progress indicator saat state dari aplikasi adalah
WeatherLoading
. - Menampilkan widget yang berisi informasi cuaca saat state dari aplikasi adalah
WeatherHasData
.
Kemudian, kita lanjutkan dengan membuat kode untuk weather_page.dart
.
Setelah itu, kembali lagi ke kode testing dan import weather_page.dart
lalu jalankan testing.
Menjalankan Aplikasi
Sampai di sini, kita sudah menyelesaikan aplikasi dengan satu fitur yaitu mendapatkan informasi cuaca terkini berdasarkan pencarian nama kota. Sekarang, kita akan coba jalankan aplikasi pada emulator.
Recap
Proses penerapan Test-Driven Development dan clean architecture memang cukup kompleks. Tapi, jika kita berpatokan pada hasil akhir yaitu minim bugs, kemudahan dalam menambah fitur, dan migrasi library (seperti state management), it’s worth!
Terakhir, kita dapat menjalankan test-coverage untuk mengetahui apakah semua kode sudah dilakukan testing. Jalankan command berikut pada direktori project di terminal:
flutter test --coverage
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
Hasilnya adalah:
Jadi, itu dia proses penerapan clean architecture dan TDD pada project Flutter. Semoga bermanfaat. Terimakasih! 😊
Complete code dapat dilihat di sini: