↑↓ seç · Enter aç · Esc kapat

Mutlu Tekin
Mutlu Tekin
← Yazılar

Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)

5 dk okuma #flutter
Dizi · 45/61 Flutter Türkçe Tutorial
  1. Bölüm 1 — Konu 1: Flutter Nedir, Mimarisi (Widget → Element → RenderObject, Skia/Impeller)
  2. Bölüm 1 — Konu 2: Ortam Kurulumu (Flutter SDK, Android Studio/VS Code, Emulator/Simulator, DevTools'a İlk Bakış)
  3. Bölüm 1 — Konu 3: İlk Proje Yapısı (`pubspec.yaml`, `lib/`, Klasör Mimarisi, `flutter create` Anatomisi)
  4. Bölüm 1 — Konu 4: Hot Reload / Hot Restart Ne Yapıyor, Neden Önemli
  5. Bölüm 2 — Konu 5: "Her Şey Widget'tır" Felsefesi, Widget Ağacı
  6. Bölüm 2 — Konu 6: StatelessWidget vs StatefulWidget + `setState()` Derinlemesine (Rebuild Mekanizması)
  7. Bölüm 2 — Konu 7: Temel Layout — Container, Row, Column, Stack, Padding, Align, Center
  8. Bölüm 2 — Konu 8: Constraint Sistemi — "Constraints Go Down, Sizes Go Up, Parent Sets Position"
  9. Bölüm 2 — Konu 9: `Expanded`, `Flexible`, `Spacer`, Intrinsic Widget'lar
  10. Bölüm 3 — Konu 10: MaterialApp/CupertinoApp, Scaffold, AppBar
  11. Bölüm 3 — Konu 11: `ListView`, `GridView`, `SingleChildScrollView` (+ Builder Pattern)
  12. Bölüm 3 — Konu 12: Text, TextStyle, Icon, Image, Asset Yönetimi
  13. Bölüm 3 — Konu 13: Custom Widget Yazma Prensipleri (Composition Over Inheritance)
  14. Bölüm 3 — Konu 14: Navigator 1.0 — Push/Pop, Named Routes
  15. Bölüm 4 — Konu 15: Form, TextField/TextFormField, GlobalKey<FormState>, Validasyon
  16. Bölüm 4 — Konu 16: Theme Sistemi (ThemeData, ColorScheme)
  17. Bölüm 4 — Konu 17: Responsive & Adaptive Tasarım (MediaQuery, LayoutBuilder, OrientationBuilder, Breakpoint Stratejileri)
  18. Bölüm 5 — Konu 18: Future/Async-Await Flutter Bağlamında, FutureBuilder
  19. Bölüm 5 — Konu 19: `Stream`, `StreamBuilder`
  20. Bölüm 5 — Konu 20: HTTP İstekleri (`http` / `dio` Paketleri)
  21. Bölüm 5 — Konu 21: JSON Serialization (Manuel → `json_serializable`/`freezed`)
  22. Bölüm 5 — Konu 22: Local Storage — `shared_preferences` → `Hive` → `sqflite`/Drift
  23. Bölüm 6 — Konu 23: InheritedWidget ve InheritedModel — "Neden setState Yetmiyor" Sorusunun Cevabı
  24. Bölüm 6 — Konu 24: Provider Paketi
  25. Bölüm 6 — Konu 25: Riverpod (Modern Yaklaşım, Provider'ın Halefi)
  26. Bölüm 6 — Konu 26: BLoC/Cubit Pattern (`flutter_bloc`)
  27. Bölüm 6 — Konu 27: GetX (Tartışmalı Ama Yaygın)
  28. Bölüm 6 — Konu 28: Karşılaştırma — Hangi Projede Hangisi?
  29. Bölüm 7 — Konu 29: Navigator 2.0 (Router, RouteInformationParser, RouterDelegate)
  30. Bölüm 7 — Konu 30: `go_router` Paketi (Pratik ve Modern Çözüm)
  31. Bölüm 7 — Konu 31: Deep Linking
  32. Bölüm 7 — Konu 32: Repository Pattern, Katmanlı Mimari (Data/Domain/Presentation)
  33. Bölüm 7 — Konu 33: Dependency Injection (`get_it`, `injectable`)
  34. Bölüm 7 — Konu 34: Clean Architecture Uyarlaması, SOLID Prensipleri
  35. Bölüm 8 — Konu 35: Implicit Animasyonlar
  36. Bölüm 8 — Konu 36: Explicit Animasyonlar (`AnimationController`, `Tween`, `Curve`)
  37. Bölüm 8 — Konu 37: Hero Animasyonları
  38. Bölüm 8 — Konu 38: `CustomPainter` / `Canvas`
  39. Bölüm 8 — Konu 39: Rive / Lottie Entegrasyonu
  40. Bölüm 9 — Konu 40: Platform Channels
  41. Bölüm 9 — Konu 41: Permission Yönetimi (`permission_handler`)
  42. Bölüm 9 — Konu 42: Kamera, Konum, Sensörler
  43. Bölüm 9 — Konu 43: Push Notification (Firebase Cloud Messaging)
  44. Bölüm 9 — Konu 44: Android/iOS Build Sistemleri
  45. Bölüm 10 — Konu 45: Test Yazımı (Unit, Widget, Integration, Golden)
  46. Bölüm 10 — Konu 46: CI/CD
  47. Bölüm 10 — Konu 47: Rebuild Optimizasyonu (`const`, `key` Kullanımı)
  48. Bölüm 10 — Konu 48: DevTools Profiling
  49. Bölüm 10 — Konu 49: Lazy Loading, Pagination, Büyük Liste Performansı
  50. Bölüm 11 — Konu 50: Firebase Ekosistemi (Auth, Firestore, Storage, Functions)
  51. Bölüm 11 — Konu 51: Supabase Alternatifi
  52. Bölüm 11 — Konu 52: GraphQL (Opsiyonel)
  53. Bölüm 11 — Konu 53: Store Yayınlama Süreci (İmzalama, Listing, Versiyonlama)
  54. Bölüm 11 — Konu 54: App Size, Obfuscation, Flavor Yönetimi
  55. Bölüm 12 — Konu 55: Flutter Web / Desktop
  56. Bölüm 12 — Konu 56: Custom `RenderObject` Yazımı
  57. Bölüm 12 — Konu 57: Engine Mimarisi Derinlemesine (Impeller vs Skia)
  58. Bölüm 12 — Konu 58: Plugin Geliştirme, pub.dev'e Paket Yayınlama
  59. Bölüm 12 — Konu 59: Monorepo Mimarisi (Melos)
  60. Bölüm 12 — Konu 60: Erişilebilirlik (Accessibility) Derinlemesine
  61. Bölüm 12 — Konu 61: Yerelleştirme (Localization / i18n)
Dizinin sayfası →
İçindekiler 9 başlık
  1. Neden Test Yazarsın?
  2. Dört Test Türü — Piramitteki Yerleri
  3. Unit Test — Saf Mantığı Test Etme
  4. Widget Test — Bir Widget'ın Davranışını Test Etme
  5. Integration Test — Uçtan Uca Gerçek Cihazda
  6. Golden Test — Piksel Piksel Görsel Doğrulama
  7. Hangi Test Türünü Ne Zaman Yazarsın?
  8. 🎯 Bu Dersten Çıkarılması Gerekenler
  9. 📝 Ödevler

Bölüm 10'a ("Kalite & Performans") hoş geldin. Şimdiye kadar kodumuzu elle çalıştırıp gözle doğruladık — bu, küçük bir uygulamada işe yarar ama proje büyüdükçe her değişiklikte her şeyi elle test etmek imkansızlaşır. Bu ders, Flutter'ın dört test türünü, ne zaman hangisini yazacağını öğretiyor.

Neden Test Yazarsın?

  • Regresyon önleme: Bir özelliği düzeltirken, başka bir özelliği bozmadığını otomatik olarak doğrularsın.
  • Güvenle refactor: Bölüm 7'de öğrendiğimiz Clean Architecture'ı hatırlarsan — domain katmanının Flutter'dan habersiz olması, testlerin hızlı ve basit olmasını sağlıyordu. Testler olmadan, o mimarinin en büyük faydası kaybolur.
  • Dokümantasyon: İyi yazılmış bir test, "bu fonksiyon şu girdide şunu yapmalı" diyen çalışan bir örnektir.

Dört Test Türü — Piramitteki Yerleri

metin
        △  Integration (az sayıda, yavaş, gerçekçi)
       △△△ Widget (orta sayıda)
     △△△△△△△ Unit (çok sayıda, hızlı, ucuz)

Bu "test piramidi", hangi türden ne kadar yazman gerektiğine dair yaygın bir kural: unit testler hızlı ve ucuz olduğu için çok sayıda, integration testler yavaş ve pahalı olduğu için az sayıda yazılır.

Unit Test — Saf Mantığı Test Etme

bash
flutter pub add dev:test  # zaten flutter test SDK'sının parçası, genelde ekstra gerekmez
dart
// lib/domain/usecases/toplam_hesapla.dart
double toplamHesapla(List<double> fiyatlar) {
  return fiyatlar.fold(0, (toplam, fiyat) => toplam + fiyat); // Dart Bölüm 5'i hatırla
}
dart
// test/toplam_hesapla_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:senin_projen/domain/usecases/toplam_hesapla.dart';

void main() {
  group('toplamHesapla', () {
    test('boş liste için 0 döner', () {
      expect(toplamHesapla([]), 0);
    });

    test('birden fazla fiyatı doğru toplar', () {
      expect(toplamHesapla([10.0, 20.0, 5.5]), 35.5);
    });
  });
}

Parça parça inceleyelim:

group(...) — Dart Bölüm 5'te öğrendiğimiz anonim fonksiyon parametresi alan bir yapı; ilgili testleri mantıksal olarak bir araya toplar (test çıktısında iç içe görünürler).

test('açıklama', () { ... }) — her test() çağrısı, tek bir senaryoyu doğrular. İsim, ne test edildiğini açıkça anlatmalı (bu, kod okunabilirliği açısından Bölüm 7'deki Single Responsibility'ye benzer bir disiplin).

expect(gercekDeger, beklenenDeger) — testin kalbi. Gerçek değer, beklenenle eşleşmezse, test başarısız olur ve hangi test olduğunu, beklenen/gerçek değerleri açıkça raporlar.

Neden bu kadar kolay? Çünkü toplamHesapla, saf bir fonksiyon — widget yok, BuildContext yok, sadece girdi alıp çıktı üreten bir Dart kodu. Bölüm 7'de domain katmanının neden Flutter'ı bilmemesi gerektiğini anlatırken bahsettiğimiz test edilebilirlik faydası, tam olarak burada somutlaşıyor.

bash
flutter test test/toplam_hesapla_test.dart  # tek dosya
flutter test                                 # tüm testler

Widget Test — Bir Widget'ın Davranışını Test Etme

dart
// test/sayac_widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:senin_projen/sayac_ekrani.dart';

void main() {
  testWidgets('artı butonuna basınca sayaç artar', (WidgetTester tester) async {
    // 1. Widget'ı "sahte" bir ekrana yerleştir
    await tester.pumpWidget(const MaterialApp(home: SayacEkrani()));

    // 2. Başlangıç durumunu doğrula
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // 3. Kullanıcı etkileşimini simüle et
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump(); // Bölüm 2 Konu 6'daki rebuild'i TETİKLE

    // 4. Yeni durumu doğrula
    expect(find.text('1'), findsOneWidget);
  });
}

testWidgets(...), normal test()'ten farklı olarak, bir WidgetTester sağlar — bu, gerçek bir cihaz/emülatör olmadan, widget ağacını bellekte simüle eden bir araçtır.

tester.pumpWidget(...) — widget'ı, runApp()'ın test ortamındaki karşılığı olarak "ekrana" yerleştirir.

find.text('0') / find.byIcon(...) — Bölüm 1'de öğrendiğimiz widget ağacı kavramını hatırla; find, bu ağaçta belirli widget'ları arayan bir "finder" API'sidir.

tester.tap(...) — bir dokunma olayını simüle eder (gerçek bir parmak dokunuşu gibi).

await tester.pump() — çok kritik bir satır. Gerçek bir uygulamada setState() çağrıldığında Flutter otomatik olarak yeniden çizer, ama test ortamında, bir sonraki "frame"in ne zaman çizileceğini sen elle tetiklemelisin — pump(), "bir kare ileri git" demektir. Bunu unutursan, tap() sonrası state değişmiş olsa bile, eski görünüm hâlâ test edilir ve testin yanlış sonuç vermesine yol açar.

expect(find.text('1'), findsOneWidget) — findsOneWidget/findsNothing/findsNWidgets(n), Dart Bölüm 7'de öğrendiğimiz enum benzeri sabitler; bulunan widget sayısını doğrular.

Integration Test — Uçtan Uca Gerçek Cihazda

dart
// integration_test/uygulama_akisi_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:senin_projen/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('giriş yap ve ana sayfaya git', (WidgetTester tester) async {
    app.main(); // GERÇEK uygulamayı başlat
    await tester.pumpAndSettle(); // tüm animasyonlar/asenkron işler bitene kadar bekle

    await tester.enterText(find.byKey(const Key('email_alani')), 'test@ornek.com');
    await tester.enterText(find.byKey(const Key('sifre_alani')), '123456');
    await tester.tap(find.text('Giriş Yap'));
    await tester.pumpAndSettle();

    expect(find.text('Hoş Geldin'), findsOneWidget);
  });
}

Widget test ile temel fark: Widget test, tek bir widget'ı izole ederek (sahte bir MaterialApp içinde) test eder. Integration test, gerçek uygulamayı, gerçek bir cihaz veya emülatörde, Navigator (Bölüm 3 Konu 14), gerçek HTTP çağrıları (Bölüm 5 Konu 20) dahil her şeyiyle çalıştırır — bu yüzden çok daha yavaş ama çok daha gerçekçidir.

find.byKey(const Key('email_alani')) — metne göre bulmak (find.text) yerine, widget'a açıkça bir kimlik (Key) vermek daha güvenilirdir — metin değişebilir (çeviri, A/B test), Key değişmez. Bu, Bölüm 8 Konu 35'te AnimatedSwitcher için öğrendiğimiz Key kavramının başka bir kullanım alanı.

pumpAndSettle() — pump()'ın "tek kare ilerlet" davranışından farklı olarak, tüm animasyonlar ve bekleyen Future'lar tamamlanana kadar otomatik olarak kareleri ilerletir — bir giriş isteğinin API'den cevap beklemesi gibi asenkron işlemler için idealdir.

Golden Test — Piksel Piksel Görsel Doğrulama

dart
testWidgets('profil kartı görsel olarak doğru', (WidgetTester tester) async {
  await tester.pumpWidget(
    const MaterialApp(home: ProfilKarti(isim: 'Ahmet', yas: 30)),
  );

  await expectLater(
    find.byType(ProfilKarti),
    matchesGoldenFile('goldens/profil_karti.png'),
  );
});

Golden test, widget'ın render edilmiş görüntüsünü, önceden kaydedilmiş bir referans resimle (golden file) piksel piksel karşılaştırır. İlk çalıştırmada --update-goldens bayrağıyla referans resmi oluşturursun:

bash
flutter test --update-goldens

Sonraki her testte, mevcut render ile bu referans karşılaştırılır — biri piksel bazında farklıysa, test başarısız olur. Bu, shouldRepaint (Bölüm 8 Konu 38) gibi "görsel çıktının doğruluğunu" doğrulayan, diğer üç test türünün yakalayamadığı bir hata sınıfını (yanlışlıkla bozulan bir layout, kaymış bir ikon) yakalar.

Pratik uyarı: Golden testler, farklı işletim sistemleri/font render motorları arasında hafif farklılıklar gösterebilir — bu yüzden genelde CI ortamında tek bir standart platformda çalıştırılır (Bölüm 10 Konu 46'da CI/CD'yi göreceğiz).

Hangi Test Türünü Ne Zaman Yazarsın?

Test Türü Test Ettiği Şey Hız Örnek
Unit Saf mantık (domain katmanı, yardımcı fonksiyonlar) Çok hızlı toplamHesapla(), bir UseCase
Widget Tek bir widget'ın davranışı (izole) Hızlı Bir formun validasyon mesajı gösterip göstermediği
Integration Uçtan uca kullanıcı akışı (gerçek cihaz) Yavaş "Giriş yap → ürün ekle → ödeme yap" akışının tamamı
Golden Görsel doğruluk (piksel eşleşmesi) Orta Bir tasarım sisteminin bileşenlerinin tam istenen görünümde olduğu

Genel tavsiye: domain/iş mantığı için bol bol unit test yaz (ucuzlar); kritik kullanıcı akışları için birkaç integration test yaz (pahalı ama değerli); UI davranışı için orta miktarda widget test; görsel tutarlılığın kritik olduğu (bir tasarım sistemi, marka bileşenleri) yerlerde golden test.


🎯 Bu Dersten Çıkarılması Gerekenler

  • Unit test, saf Dart mantığını test eder — Bölüm 7'deki Clean Architecture'ın domain katmanı, bu testleri kolay ve hızlı yapar.
  • Widget test (testWidgets + WidgetTester), tek bir widget'ı izole test eder; pump() ile bir sonraki kareyi elle tetiklemen gerekir.
  • Integration test, gerçek uygulamayı uçtan uca (gerçek Navigator, HTTP çağrıları dahil) test eder; pumpAndSettle() asenkron işlemlerin bitmesini bekler.
  • Golden test, widget'ın render edilmiş görüntüsünü referans bir resimle piksel piksel karşılaştırır.
  • Test piramidi: çok sayıda unit test, orta sayıda widget test, az sayıda integration test.
  • find.byKey(), metne göre bulmaktan (find.text()) daha güvenilir bir widget bulma yöntemidir.

📝 Ödevler

  • [ ] Bölüm 7'de yazdığın (ya da benzer) bir UseCase sınıfı için en az 3 senaryoyu kapsayan unit testler yaz.
  • [ ] Bir form widget'ı için, boş alan gönderildiğinde hata mesajının göründüğünü doğrulayan bir widget test yaz.
  • [ ] integration_test paketini kur, uygulamanın 2 ekranlı basit bir akışını (örn. liste → detay) uçtan uca test et.
  • [ ] Bir widget için golden test yaz, --update-goldens ile referans resmi oluştur, sonra widget'ı bilerek bozup testin başarısız olduğunu gözlemle.
  • [ ] Kendi cümlelerinle, "test piramidinin neden çok integration test yerine çok unit test önerdiğini" açıkla.

Sıradaki konu: Bölüm 10 — Konu 46: CI/CD