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

Aditya Rohman
9 min readJan 24, 2022
Photo by Waranont (Joe) on Unsplash

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.

Diagram Clean Architecture

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 Test-Driven Development

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.dart
test
├── 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.

Hasil testing GetCurrentWeather usecase

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.

Hasil testing weather model

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.

Hasil testing remote data source

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.

Hasil testing weather repository impl

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.

Hasil testing weather bloc

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 menjadi WeatherLoading ketika selesai memasukkan data ke widget TextField.
  • 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.

Hasil testing weather page

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.

Hasil akhir

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:

Hasil test-coverage

Jadi, itu dia proses penerapan clean architecture dan TDD pada project Flutter. Semoga bermanfaat. Terimakasih! 😊

Complete code dapat dilihat di sini:

--

--

Aditya Rohman

Flutter Developer at Koltiva,  iOS Development Enthusiast