🧪
Testing - Mobile (Flutter, Dart)

Hasil Riset Faris

Referensi:
Tutorial Testing pada Flutter
React Bits (Orang India) - https://www.youtube.com/playlist?list=PLUiueC0kTFqLvpFk_Zg55geh_TBTKnbnA

Ada penjelasan dan praktik cara melakukan unit, widget, dan integration testing menggunakan Flutter Driver. Ada juga cara penggunaan Mockito serta cara melakukan testing apabila terhubung dengan Firebase Auth.

Notes: Orang India dan jelasinnya lebih ke arah kodenya langsung.

Robert Brunhage - https://www.youtube.com/watch?v=RDY6UYh-nyg

Ada penjelasan unit, widget, dan integration testing menggunakan Flutter Driver. Penjelasannya lebih bagus dibanding referensi sebelumnya. Ada juga penggunaan Mockito dan beberapa petunjuk apa yang harus diubah dalam suatu kode agar bisa testable (misal: konstruktornya ditambahkan parameter tambahan agar bisa memasukkan injeksi kelas yang sudah di-mock).

Flutter Explained - https://www.youtube.com/watch?v=WPEsnJgW99M

Integration Testing menggunakan package integration_test (Ga dijelasin sih alasan pakai package ini, tapi di video referensi berikutnya dijelasin). Dijelasin juga gimana cara agar Integration Testing bisa dipisah-pisah file-nya (beserta pros & cons karena kalau di Flutter Driver gabisa terpisah). Integration test-nya lebih advanced dengan penggunaan matchSchematics untuk beberapa widget yang lebih rumit.

Reso Coder - https://www.youtube.com/watch?v=izajHHFSa8o

Penjelasan Integration Testing-nya keren banget. Dijelasin juga kenapa Integration Testing lebih baik pakai package integration_test daripada Flutter Driver bawaan. Ada juga beberapa best practice dalam Integration Testing. Selain itu, ada juga tutorial cara Integrasi CI/CD (CI aja sih sebenarnya karena utk testing) menggunakan Codemagic (untuk otomasinya) dan Firebase Test Lab (untuk testing-nya) beserta cara membuat scriptnya di Codemagic (basically post-build script dengan bash dan gcloud).

Tutorial setup CI/CD
Reso Coder - https://www.youtube.com/watch?v=izajHHFSa8o

Udah dijelasin di tutorial testing di atas karena dia ngecover testing hingga CI di Codemagic dan integrasi dengan Firebase Test Lab.

Flutter Explained - https://www.youtube.com/watch?v=kR0N_Ecv_b0

Ngejelasin pros & cons CI/CD menggunakan Github Action dan Codemagic. Ada juga praktik ngebuat Github Action untuk ngebuat testing otomatis (pakai Flutter Action, cuman testing aja sih, CDnya cuman build apk), dan juga praktik ngebuat CI/CD di Codemagic (testing dan ngejelasin bisa CD ke mana aja integrasinya, termasuk notif ke Slack atau email).

Notion image
Fireship - https://www.youtube.com/watch?v=eB0nUzAI7M8

Sebenarnya ini lebih ke tutorial Github Actions sih, apa aja yang bisa dilakuin dengan Github Action dsb dan cara integrasi dengan Firebase (misal: deploy ke Firebase otomatis, export firestore tiap jadwal tertentu, dsb).

Firebase - https://www.youtube.com/watch?v=scfOk5SgrKU

Seminar Fastlane yang ngejelasin masalah-masalah CD dan tools yang ada di Fastlane. Penjelasannya bagus dan ada praktik juga bahkan sampai membuat plugin Fastlane manual, namun sayangnya target seminarnya kayaknya lebih ke arah iOS Developers jadi contohnya kebanyakan untuk iOS (walaupun Fastlane sebenarnya support iOS dan Android).

Flutter Testing

Ada 3 jenis testing pada Flutter yaitu unit testing, widget testing, dan integration testing. Semuanya memiliki pros dan cons-nya masing-masing, dan pedomannya adalah jumlah unit testing > widget testing > integration testing.

Referensi Official:

Dependencies

dev_dependencies:
  test:              # Core testing library
  flutter_test:      # Widget testing library
    sdk: flutter
  integration_test:  # Integration testing library
    sdk: flutter
  mockito:           # Mocking library

# OPTIONAL
  build_runner:      # To build Mockito's GenerateMocks classes
  flutter_drive:     # Old version of Flutter's integration testing library
   sdk: flutter

Unit Testing

Unit testing adalah jenis testing yang digunakan untuk melakukan testing terhadap suatu method atau class. External dependencies biasanya di-mock. Idealnya, unit testing jumlahnya paling banyak dibanding widget dan integration testing, misalnya untuk mengetes sistem autentikasi apakah method yang dipanggil sesuai, apakah pemrosesan data sesuai, dsb. Bedanya unit testing dengan widget testing adalah pada unit testing tidak diperlukan WidgetTester karena yang di-test adalah method/class langsung.

Referensi unit testing:

Example (from doc)

// lib/counter.dart
class Counter {
  int value = 0;

  void increment() => value++;
  void decrement() => value--;
}

// test/counter_test.dart
import 'package:test/test.dart';
import 'package:counter_app/counter.dart';

void main() {
  group('Counter', () {
    test('value should start at 0', () {
      expect(Counter().value, 0);
    });

    test('value should be incremented', () {
      final counter = Counter();
      counter.increment();

      expect(counter.value, 1);
    });

    test('value should be decremented', () {
      final counter = Counter();
      counter.decrement();

      expect(counter.value, -1);
    });
  });
}

Example with Firebase Auth Mock

Reference: Youtube and Github

Intinya, FirebaseAuth instance (kalau di courier_mobile/partner_mobile ada di services/auth.dart) harus di-mock. Pada contoh dibawah ini, bisa dibilang UserRepository mirip seperti gabungan antara auth.dart dan UserModel ketika melakukan autentikasi pada root.dart dan login pada Login.dart.

// Firebase Auth test
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_auth/model/user_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:rxdart/rxdart.dart';

class MockFirebaseAuth extends Mock implements FirebaseAuth {}

class MockFirebaseUser extends Mock implements FirebaseUser {}

class MockAuthResult extends Mock implements AuthResult {}

void main() {
  // Setup keseluruhan test: melakukan mock terhadap FirebaseAuth dan FirebaseUser.
  MockFirebaseAuth _auth = MockFirebaseAuth();
  BehaviorSubject<MockFirebaseUser> _user = BehaviorSubject<MockFirebaseUser>();

  when(_auth.onAuthStateChanged).thenAnswer((_) {
    return _user;
  });

  // Memasukkan _auth instance yang sudah di-mock
  UserRepository _repo = UserRepository.instance(auth: _auth);

  group('user repository test', () {
    // Setup untuk group test ini: 
    // definisikan email dan password yang benar dan yang salah.
    when(_auth.signInWithEmailAndPassword(email: "email", password: "password"))
        .thenAnswer((_) async {
      _user.add(MockFirebaseUser());
      return MockAuthResult();
    });
    when(_auth.signInWithEmailAndPassword(email: "mail", password: "pass"))
        .thenThrow(() {
      return null;
    });

    test("sign in with email and password", () async {
      bool signedIn = await _repo.signIn("email", "password");

      expect(signedIn, true);
      expect(_repo.status, Status.Authenticated);
    });

    test("sign in fails with incorrect email and password", () async {
      bool signedIn = await _repo.signIn("mail", "pass");

      expect(signedIn, false);
      expect(_repo.status, Status.Unauthenticated);
    });

    test('sign out', () async {
      await _repo.signOut();

      expect(_repo.status, Status.Unauthenticated);
    });
  });
}

Widget Testing

Seperti namanya, widget testing digunakan untuk mengetes suatu widget. Karena testing-nya hanya mencakup satu widget saja, biasanya hanya berkaitan dengan satu screen. Berguna untuk mengetes hal-hal terkait UI dan validasi input.

Referensi widget testing:

Example (in doc)

// lib/main.dart 
class MyWidget extends StatelessWidget {
  const MyWidget({
    Key? key,
    required this.title,
    required this.message,
  }) : super(key: key);

  final String title;
  final String message;

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: Scaffold(
        appBar: AppBar(
          title: Text(title),
        ),
        body: Center(
          child: Text(message),
        ),
      ),
    );
  }
}

// test/widget_test.dart
void main() {
  testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
    await tester.pumpWidget(const MyWidget(title: 'T', message: 'M'));

    final titleFinder = find.text('T');
    final messageFinder = find.text('M');

    expect(titleFinder, findsOneWidget);
    expect(messageFinder, findsOneWidget);
  });
}

Example with Firebase Auth and Provider

Reference: Youtube and Github

Notes: Pastikan setelah menekan tombol, atau ketika mentrigger event yang memiliki callbacks, setelahnya harus memakai pump() terlebih dahulu agar testernya dapat menunggu seluruh callback telah selesai dijalankan. Apabila terdapat method yang bersifat async atau memiliki animasi, sebaiknya pump() digantikan dengan pumpAndSettle().

import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_auth/model/user_repository.dart';
import 'package:flutter_auth/ui/pages/login.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';
import 'package:rxdart/rxdart.dart';

class MockUserRepository extends Mock implements UserRepository {
  final MockFirebaseAuth auth;
  MockUserRepository({this.auth});
}

class MockFirebaseAuth extends Mock implements FirebaseAuth {}

class MockFirebaseUser extends Mock implements FirebaseUser {}

void main() {
// Setup keseluruhan test: melakukan mock terhadap FirebaseAuth, FirebaseUser, dan Repository (auth service + providernya)
  MockFirebaseAuth _auth = MockFirebaseAuth();
  BehaviorSubject<MockFirebaseUser> _user = BehaviorSubject<MockFirebaseUser>();

  MockUserRepository _repo;
  _repo = MockUserRepository(auth: _auth);

  // Mock Provider menggunakan repo yang telah di-mock
  // Jadikan function agar bisa dapat dipakai di bagian testing manapun
  Widget _makeTestable(Widget child) {
    return ChangeNotifierProvider<UserRepository>.value(
      value: _repo,
      child: MaterialApp(
        home: child,
      ),
    );
  }

  // Cari widget berdasarkan key
  var emailField = find.byKey(Key("email-field"));
  var passwordField = find.byKey(Key("password-field"));
  var signInButton = find.text("Sign In");

  group("login page test", () {
    // Setup untuk group test ini: definisikan email dan password yang benar
    when(_repo.signIn("[email protected]", "password")).thenAnswer((_) async {
      return true;
    });

    testWidgets('email, password and button are found',
        (WidgetTester tester) async {
      await tester.pumpWidget(_makeTestable(LoginPage()));

      expect(emailField, findsOneWidget);
      expect(passwordField, findsOneWidget);
      expect(signInButton, findsOneWidget);
    });

    testWidgets("validates empty email and password",
        (WidgetTester tester) async {
      await tester.pumpWidget(_makeTestable(LoginPage()));

      await tester.tap(signInButton);
      await tester.pump();

      expect(find.text("Please Enter Email"), findsOneWidget);
      expect(find.text("Please Enter Password"), findsOneWidget);
    });

    testWidgets("calls sign in method when email and password is entered",
        (WidgetTester tester) async {
      await tester.pumpWidget(_makeTestable(LoginPage()));

      await tester.enterText(emailField, "[email protected]");
      await tester.enterText(passwordField, "password");
      await tester.tap(signInButton);
      await tester.pump();

      verify(_repo.signIn("[email protected]", "password")).called(1);
    });
  });
}

Integration Testing

Integration testing adalah testing dengan menggunakan emulator atau HP asli untuk mengetes UI Flow dari suatu aplikasi. Bisa dibilang, tes jenis ini seakan mensimulasikan pengguna yang menggunakan aplikasi secara utuh. Tes ini memakan waktu paling lama namun dapat menjadi pelengkap jenis tes-tes lainnya karena dapat melakukan tes yang lebih menyeluruh dan menutupi edge cases yang tidak tertangani pada jenis testing lain. Pada Android native, tes ini juga dikenal sebagai instrumentation test.

Referensi integration testing:

Example (in doc)

Notes: Kalau di widget testing pakai pump(), kalau di integration testing pakai pumpAndSettle(). pumpAndSettle() pada dasarnya menunggu callback dan animasi selesai.

// test_driver/integration_test.dart
import 'package:integration_test/integration_test_driver.dart';

Future<void> main() => integrationDriver();


// integration_test/mytest.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';

// The application under test.
import 'package:integration_test_example/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('end-to-end test', () {
    testWidgets('tap on the floating action button; verify counter',
        (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      // Finds the floating action button to tap on.
      final Finder fab = find.byTooltip('Increment');

      // Emulate a tap on the floating action button.
      await tester.tap(fab);

      await tester.pumpAndSettle();

      expect(find.text('1'), findsOneWidget);
    });
  });
}

Integration Testing with Firebase Auth

Ternyata ada 3 cara untuk melakukan integration testing dengan Firebase Auth:

  1. Biarin aja (gausah di-mock). Pakai akun testing yang sudah di-setup di Firebase Auth. Pros: simple Cons: tergantung sama 3rd party (Firebase) dan koneksi internet
  1. Mock Firebase Authnya. Ada library yang bisa ngebantu juga (lengkapnya disini). Pros: Ga tergantung dengan 3rd party dan internet connection Cons: lebih ribet dibanding cara 1 (musti nge-mock berbagai macam method dan property, makanya lebih mudah pakai library aja)
  1. Pakai method-method yang sudah disediain sama Firebase Auth (terutama Phone Auth) Pros: Official, ada di dalam dokumentasi Firebase Phone Auth Cons: Entah kenapa jarang ada yang menggunakan ini, sehingga resourcesnya agak lebih terbatas

Mocking

Mocking adalah teknik mengemulasi suatu dependensi (bisa berupa library, method, atau class) agar tes dapat berjalan tanpa benar-benar menggunakan dependensi tersebut sehingga dapat berjalan dengan lebih reliable dan lebih terukur. Misal, dengan menggunakan mock kita dapat mengemulasikan fetch data dari suatu API dan mengembalikan data dummy yang kita inginkan sehingga tes yang kita buat tidak bergantung pada koneksi internet dan API eksternal tersebut.

Referensi mocking:

Best Practices