Flutter Testing Rehberi: Unit, Widget ve Integration Test

14 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Flutter testingFlutter unit testFlutter widget testFlutter integration testFlutter test strategyFlutter test coverageDart testCI testing Flutter

# Flutter Test Rehberi: Unit, Widget ve Integration Testleri

Test yazmak hızınızı düşürmez — tam tersine, üretimde hata bulmanın ve düzeltmenin maliyetini dramatik şekilde azaltır. İyi yapılandırılmış bir test altyapısı, refactor sırasında güven verir ve kodunuz için canlı bir dokümantasyon görevi görür.

Test Piramidi

Test piramidi, sağlıklı bir test stratejisinin temelidir. Mantık basit: altta çok sayıda hızlı ve ucuz test, üstte az sayıda yavaş ve kapsamlı test.

Unit Testler

Unit testler tek bir fonksiyonu, metodu veya sınıfı izole şekilde doğrular. En hızlı çalışan, en kolay yazılan testlerdir ve test takımınızın büyük bölümünü oluşturmalıdır.

dart
class=class="code-string">"code-comment">// test/services/price_calculator_test.dart
import class="code-string">'package:flutter_test/flutter_test.dart';
import class="code-string">'package:my_app/services/price_calculator.dart';

void main() {
  late PriceCalculator calculator;

  setUp(() {
    calculator = PriceCalculator();
  });

  group(class="code-string">'PriceCalculator', () {
    test(class="code-string">'yüzdelik indirimi doğru uygular', () {
      final result = calculator.applyDiscount(class="code-number">100.0, percentage: class="code-number">20);
      expect(result, class="code-number">80.0);
    });

    test(class="code-string">'negatif fiyata izin vermez', () {
      final result = calculator.applyDiscount(class="code-number">10.0, percentage: class="code-number">150);
      expect(result, class="code-number">0.0);
    });

    test(class="code-string">'geçersiz yüzde değerinde hata fırlatır', () {
      expect(
        () => calculator.applyDiscount(class="code-number">100.0, percentage: -class="code-number">5),
        throwsA(isA<ArgumentError>()),
      );
    });

    test(class="code-string">'vergiyi doğru hesaplar', () {
      final result = calculator.withTax(class="code-number">100.0, taxRate: class="code-number">0.18);
      expect(result, closeTo(class="code-number">118.0, class="code-number">0.01));
    });
  });
}

Widget Testler

Widget testler, tek bir widget'ı veya küçük bir widget ağacını render edip davranışını doğrular — dokunma olayları, metin gösterimi, durum değişiklikleri — gerçek bir cihaza ihtiyaç duymadan.

dart
class=class="code-string">"code-comment">// test/widgets/login_form_test.dart
import class="code-string">'package:flutter/material.dart';
import class="code-string">'package:flutter_test/flutter_test.dart';
import class="code-string">'package:my_app/widgets/login_form.dart';

void main() {
  group(class="code-string">'LoginForm', () {
    testWidgets(class="code-string">'e-posta boşken doğrulama hatası gösterir',
        (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(home: Scaffold(body: LoginForm())),
      );

      class=class="code-string">"code-comment">// Hiçbir şey girmeden gönder butonuna bas
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();

      expect(find.text(class="code-string">'E-posta gereklidir'), findsOneWidget);
    });

    testWidgets(class="code-string">'doğru bilgilerle onSubmit çağrılır',
        (WidgetTester tester) async {
      String? gonderilenEmail;
      String? gonderilenSifre;

      await tester.pumpWidget(
        MaterialApp(
          home: Scaffold(
            body: LoginForm(
              onSubmit: (email, password) {
                gonderilenEmail = email;
                gonderilenSifre = password;
              },
            ),
          ),
        ),
      );

      await tester.enterText(
        find.byKey(const Key(class="code-string">'email_field')),
        class="code-string">'kullanici@ornek.com',
      );
      await tester.enterText(
        find.byKey(const Key(class="code-string">'password_field')),
        class="code-string">'guvenliSifre123',
      );
      await tester.tap(find.byType(ElevatedButton));
      await tester.pumpAndSettle();

      expect(gonderilenEmail, class="code-string">'kullanici@ornek.com');
      expect(gonderilenSifre, class="code-string">'guvenliSifre123');
    });

    testWidgets(class="code-string">'şifre görünürlüğünü değiştirir',
        (WidgetTester tester) async {
      await tester.pumpWidget(
        const MaterialApp(home: Scaffold(body: LoginForm())),
      );

      class=class="code-string">"code-comment">// Şifre varsayılan olarak gizli olmalı
      final passwordField = tester.widget<TextField>(
        find.byKey(const Key(class="code-string">'password_field')),
      );
      expect(passwordField.obscureText, isTrue);

      class=class="code-string">"code-comment">// Görünürlük simgesine bas
      await tester.tap(find.byIcon(Icons.visibility_off));
      await tester.pump();

      final updatedField = tester.widget<TextField>(
        find.byKey(const Key(class="code-string">'password_field')),
      );
      expect(updatedField.obscureText, isFalse);
    });
  });
}

Integration Testler

Integration testler gerçek bir cihazda veya emülatörde çalışır ve kullanıcı akışlarını uçtan uca doğrular. Daha yavaştırlar ama unit ve widget testlerin yakalayamadığı sorunları ortaya çıkarır.

dart
class=class="code-string">"code-comment">// integration_test/app_test.dart
import class="code-string">'package:flutter_test/flutter_test.dart';
import class="code-string">'package:integration_test/integration_test.dart';
import class="code-string">'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group(class="code-string">'uçtan uca: kimlik doğrulama akışı', () {
    testWidgets(class="code-string">'kullanıcı giriş yapıp ana ekranı görebilir',
        (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      class=class="code-string">"code-comment">// Kimlik bilgilerini gir
      await tester.enterText(
        find.byKey(const Key(class="code-string">'email_field')),
        class="code-string">'test@ornek.com',
      );
      await tester.enterText(
        find.byKey(const Key(class="code-string">'password_field')),
        class="code-string">'sifre123',
      );

      class=class="code-string">"code-comment">// Gönder
      await tester.tap(find.byKey(const Key(class="code-string">'login_button')));
      await tester.pumpAndSettle(const Duration(seconds: class="code-number">3));

      class=class="code-string">"code-comment">// Ana ekrana yönlendirildiğini doğrula
      expect(find.text(class="code-string">'Tekrar hoş geldiniz!'), findsOneWidget);
      expect(find.byType(BottomNavigationBar), findsOneWidget);
    });
  });
}

Mockito ile Mocklama

Gerçek uygulamalar API'lere, veritabanlarına ve platform servislerine bağımlıdır. Mocklama, bu bağımlılıkları kontrollü ikamelerle değiştirerek testlerinizi hızlı, deterministik ve çevrimdışı çalışabilir hale getirir.

dart
class=class="code-string">"code-comment">// test/repositories/user_repository_test.dart
import class="code-string">'package:flutter_test/flutter_test.dart';
import class="code-string">'package:mockito/annotations.dart';
import class="code-string">'package:mockito/mockito.dart';
import class="code-string">'package:my_app/services/api_client.dart';
import class="code-string">'package:my_app/models/user.dart';
import class="code-string">'package:my_app/repositories/user_repository.dart';

@GenerateMocks([ApiClient])
import class="code-string">'user_repository_test.mocks.dart';

void main() {
  late MockApiClient mockApiClient;
  late UserRepository repository;

  setUp(() {
    mockApiClient = MockApiClient();
    repository = UserRepository(apiClient: mockApiClient);
  });

  group(class="code-string">'UserRepository', () {
    test(class="code-string">'API başarılı olduğunda kullanıcı döner', () async {
      when(mockApiClient.get(class="code-string">'/users/class="code-number">1')).thenAnswer(
        (_) async => {class="code-string">'id': class="code-number">1, class="code-string">'name': class="code-string">'Ayşe', class="code-string">'email': class="code-string">'ayse@test.com'},
      );

      final user = await repository.getUser(class="code-number">1);

      expect(user.name, class="code-string">'Ayşe');
      expect(user.email, class="code-string">'ayse@test.com');
      verify(mockApiClient.get(class="code-string">'/users/class="code-number">1')).called(class="code-number">1);
    });

    test(class="code-string">'API class="code-number">404 döndüğünde UserNotFoundException fırlatır', () async {
      when(mockApiClient.get(class="code-string">'/users/class="code-number">999')).thenThrow(
        ApiException(statusCode: class="code-number">404, message: class="code-string">'Bulunamadı'),
      );

      expect(
        () => repository.getUser(class="code-number">999),
        throwsA(isA<UserNotFoundException>()),
      );
    });

    test(class="code-string">'ilk çağrıdan sonra kullanıcıyı önbelleğe alır', () async {
      when(mockApiClient.get(class="code-string">'/users/class="code-number">1')).thenAnswer(
        (_) async => {class="code-string">'id': class="code-number">1, class="code-string">'name': class="code-string">'Ayşe', class="code-string">'email': class="code-string">'ayse@test.com'},
      );

      await repository.getUser(class="code-number">1);
      await repository.getUser(class="code-number">1);

      class=class="code-string">"code-comment">// Önbellekleme sayesinde API yalnızca bir kez çağrılmalı
      verify(mockApiClient.get(class="code-string">'/users/class="code-number">1')).called(class="code-number">1);
    });
  });
}

Test dosyanızı yazdıktan sonra `.mocks.dart` dosyasını oluşturmak için `dart run build_runner build` komutunu çalıştırın.

Golden Testler

Golden testler (anlık görüntü testleri olarak da bilinir), render edilmiş bir widget'ı görüntü olarak kaydeder ve gelecekteki render'ları bu referans görüntüyle karşılaştırır. İstenmeyen görsel regresyonları yakalamak için son derece değerlidir.

dart
class=class="code-string">"code-comment">// test/widgets/profile_card_golden_test.dart
import class="code-string">'package:flutter/material.dart';
import class="code-string">'package:flutter_test/flutter_test.dart';
import class="code-string">'package:my_app/widgets/profile_card.dart';

void main() {
  group(class="code-string">'ProfileCard golden testleri', () {
    testWidgets(class="code-string">'varsayılan görünümle eşleşir', (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData.light(),
          home: const Scaffold(
            body: ProfileCard(
              name: class="code-string">'Ayşe Yılmaz',
              role: class="code-string">'Flutter Geliştirici',
              avatarUrl: null,
            ),
          ),
        ),
      );

      await expectLater(
        find.byType(ProfileCard),
        matchesGoldenFile(class="code-string">'goldens/profile_card_default.png'),
      );
    });

    testWidgets(class="code-string">'koyu tema görünümüyle eşleşir',
        (WidgetTester tester) async {
      await tester.pumpWidget(
        MaterialApp(
          theme: ThemeData.dark(),
          home: const Scaffold(
            body: ProfileCard(
              name: class="code-string">'Ayşe Yılmaz',
              role: class="code-string">'Flutter Geliştirici',
              avatarUrl: null,
            ),
          ),
        ),
      );

      await expectLater(
        find.byType(ProfileCard),
        matchesGoldenFile(class="code-string">'goldens/profile_card_dark.png'),
      );
    });
  });
}

Referans görüntüleri oluşturmak için `flutter test --update-goldens` komutunu çalıştırın. Sonraki çalışmalarda Flutter, yeni render'ları bu referanslarla piksel piksel karşılaştırır. Golden dosyalarını versiyon kontrolünde tutun ki tüm ekip aynı referansı kullansın.

Test Organizasyonu

Tutarlı bir test yapısı, proje büyüdükçe inanılmaz fark yaratır. İşte benim uyguladığım pratikler.

Dosya İsimlendirme ve Yapısı

`lib/` klasör yapınızı `test/` içinde aynalayın. `lib/services/auth_service.dart` dosyanız varsa, testi `test/services/auth_service_test.dart` konumunda olmalı. Bu düzen, herhangi bir dosyanın testini bulmayı son derece kolaylaştırır.

test/
  services/
    auth_service_test.dart
    price_calculator_test.dart
  repositories/
    user_repository_test.dart
  widgets/
    login_form_test.dart
    profile_card_test.dart
    profile_card_golden_test.dart
  helpers/
    test_data.dart
    pump_app.dart
integration_test/
  app_test.dart

Testleri Gruplama

İlişkili doğrulamaları düzenlemek için `group()` kullanın. Birden fazla metodu olan karmaşık sınıflar için grupları iç içe yerleştirin.

dart
group(class="code-string">'AuthService', () {
  group(class="code-string">'login', () {
    test(class="code-string">'geçerli kimlik bilgileriyle başarılı olur', () { class=class="code-string">"code-comment">/* ... */ });
    test(class="code-string">'yanlış şifreyle başarısız olur', () { class=class="code-string">"code-comment">/* ... */ });
    test(class="code-string">'class="code-number">5 başarısız denemeden sonra hız sınırı uygular', () { class=class="code-string">"code-comment">/* ... */ });
  });

  group(class="code-string">'logout', () {
    test(class="code-string">'saklanan tokeni temizler', () { class=class="code-string">"code-comment">/* ... */ });
    test(class="code-string">'giriş ekranına yönlendirir', () { class=class="code-string">"code-comment">/* ... */ });
  });
});

Paylaşılan Test Yardımcıları

Tekrarı önlemek için yeniden kullanılabilir kurulumları yardımcı dosyalara çıkarın.

dart
class=class="code-string">"code-comment">// test/helpers/pump_app.dart
import class="code-string">'package:flutter/material.dart';
import class="code-string">'package:flutter_test/flutter_test.dart';

extension PumpApp on WidgetTester {
  Future<void> pumpApp(Widget widget) {
    return pumpWidget(
      MaterialApp(
        home: Scaffold(body: widget),
      ),
    );
  }
}
dart
class=class="code-string">"code-comment">// test/helpers/test_data.dart
import class="code-string">'package:my_app/models/user.dart';

class TestData {
  static User get ornekKullanici => User(
    id: class="code-number">1,
    name: class="code-string">'Test Kullanıcı',
    email: class="code-string">'test@ornek.com',
  );

  static List<User> get ornekKullanicilar => [
    ornekKullanici,
    User(id: class="code-number">2, name: class="code-string">'Diğer Kullanıcı', email: class="code-string">'diger@ornek.com'),
  ];
}

Neleri Test Etmemeli

Her şeyi test etmek ne pratik ne de değerlidir. Neyi atlayacağınızı bilmek, neyi test edeceğinizi bilmek kadar önemlidir.

  • **Flutter framework iç yapısı.** `Text('merhaba')` widget'ının "merhaba" yazısını render ettiğini test etmeyin. Flutter'ın kendi testleri bunu kapsar.
  • **Oluşturulan kod.** `build_runner`, `json_serializable` veya `freezed` tarafından üretilen kod makine tarafından oluşturulmuştur. Test etmek değer katmadan gürültü ekler.
  • **Basit getter ve setter'lar.** Sadece bir alanı döndüren getter'da doğrulanacak mantık yoktur.
  • **Üçüncü parti paket davranışı.** HTTP veya depolama için kullandığınız paketin kendi test takımına güvenin. Sınırda mocklayıp kendi mantığınızı test edin.
  • **Birebir uygulama detayları.** Uygulamayı satır satır aynalayan testler, her refactor'da kırılır ama gerçek hata yakalamaz. Davranışı test edin, uygulamayı değil.
  • Enerjinizi iş mantığına, uç durumlara, hata yönetimine ve kritik kullanıcı akışlarına yoğunlaştırın.

    CI Entegrasyonu: GitHub Actions

    Testleri CI'da otomatikleştirmek, ekip projelerinde tartışmasız bir gerekliliktir. İşte her pull request'te Flutter testlerini çalıştıran bir GitHub Actions iş akışı.

    yaml
    # .github/workflows/flutter_tests.yml
    name: Flutter Testleri
    
    on:
      pull_request:
        branches: [main, develop]
      push:
        branches: [main]
    
    jobs:
      test:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
    
          - uses: subosiro/flutter-action@v2
            with:
              flutter-version: '3.24.0'
              channel: 'stable'
              cache: true
    
          - name: Bağımlılıkları yükle
            run: flutter pub get
    
          - name: Formatlama kontrolü
            run: dart format --set-exit-if-changed .
    
          - name: Statik analiz
            run: flutter analyze --fatal-infos
    
          - name: Unit ve widget testlerini coverage ile çalıştır
            run: flutter test --coverage
    
          - name: Minimum coverage kontrolü
            run: |
              COVERAGE=$(lcov --summary coverage/lcov.info 2>&1 | grep 'lines' | sed 's/.*: "code-comment">//' | sed 's/%.*//')
              echo "Mevcut coverage: $COVERAGE%"
              if (( $(echo "$COVERAGE < 70" | bc -l) )); then
                echo "Coverage $COVERAGE%, %70 eşiğinin altında"
                exit 1
              fi
    
          - name: Coverage raporunu yükle
            uses: actions/upload-artifact@v4
            with:
              name: coverage-report
              path: coverage/lcov.info

    Bu pipeline formatlama kontrolü, statik analiz, coverage takipli test çalıştırma ve minimum coverage eşiği zorunluluğu içerir. Başarısız bir test merge'i engeller — tam da istediğiniz şey.

    Kişisel Yaklaşımım

    Projelerimde iş mantığı ve repository katmanlarında yüksek coverage hedefliyorum — genellikle %85'in üzerinde — widget test coverage'ını ise formlar ve navigasyon akışları gibi etkileşim yoğun bileşenlere odaklıyorum. %100 coverage peşinde koşmuyorum; son %15 genellikle basit kodu kapsar ve bakım maliyeti, sağladığı faydayı aşar.

    Bir hatayı düzeltmeden önce mutlaka test yazarım: önce hatayı başarısız bir testle yeniden üretirim, sonra kodu düzeltip testin yeşile döndüğünü izlerim. Bu yaklaşım, aynı hatanın bir daha geri gelmeyeceğini garanti eder.

    Sıfırdan başlayan projelerde CI'ı test zorunluluğuyla birlikte ilk gün kurarım. Testleri sonradan eklemenin maliyeti, yazarken eklemenin maliyetinden her zaman daha yüksektir.

    Sonuç

    Dengeli bir test takımı — çok sayıda unit test, hedefli widget testler ve bir avuç integration test — hızlı gönderi yapma güvenini, bir şeyleri bozmadan verir. Bunu CI zorunluluğuyla birleştirdiğinizde, tek kişilik projelerden büyük ekiplere ölçeklenen bir iş akışı elde edersiniz.

    İsterseniz mevcut projeniz için gerçekçi bir test coverage planı oluşturabilirim.

    İlgili Makaleler

    Flutter Projeniz mi Var?

    iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

    İletişime Geç