Test yazmak, yazilim gelistirmenin en onemli ama en cok ihmal edilen bolumudur. "Sonra yazarim" diyerek ertelenen testler, production'da patlayan bug'larla geri doner. Flutter, test altyapisi konusunda cok guclu araclar sunuyor — unit test'ten golden test'e kadar her seviyede test yazabilirsiniz.
Bu rehberde Flutter'in test ekosistemini bastan sona ele alacagiz. Her test tipi icin ne zaman kullanilacagini, nasil yazilacagini ve CI pipeline'a nasil entegre edilecegini ogreneceksiniz.
Not: Tum ornekler flutter_test, mockito ve integration_test paketleri ile test edilmistir.
Icindekiler
- Test Piramidi
- Unit Test
- Widget Test
- Integration Test
- Golden Test
- Mocking Stratejileri
- Async Test Kaliplari
- Test Coverage
- CI/CD Entegrasyonu
- TDD ile Flutter
- Best Practices
- Sonuc ve Oneriler
1. Test Piramidi
Flutter'da test piramidi klasik yazilim test piramidinini takip eder:
Test Tipleri Karsilastirmasi
Test Tipi | Hiz | Guvenilirlik | Bakim Maliyeti | Kapsam |
|---|---|---|---|---|
**Unit Test** | Cok Hizli | Yuksek | Dusuk | Tek fonksiyon/sinif |
**Widget Test** | Hizli | Yuksek | Orta | Tek widget |
**Integration Test** | Yavas | Orta | Yuksek | Tum uygulama |
**Golden Test** | Orta | Dusuk | Yuksek | Gorsel karsilastirma |
**E2E Test** | Cok Yavas | Orta | Cok Yuksek | Kullanici senaryosu |
Onerilen Dagitim: %70 Unit + %20 Widget + %10 Integration
2. Unit Test
Unit test, tek bir fonksiyon veya sinifin davranisini test eder:
dart
1// lib/utils/validators.dart2class Validators {3 static String? validateEmail(String? value) {4 if (value == null || value.isEmpty) return 'Email gerekli';5 final regex = RegExp(r'^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\.[a-zA-Z]+');6 if (!regex.hasMatch(value)) return 'Gecersiz email formati';7 return null;8 }9 10 static String? validatePassword(String? value) {11 if (value == null || value.isEmpty) return 'Sifre gerekli';12 if (value.length < 8) return 'Sifre en az 8 karakter olmali';13 if (!value.contains(RegExp(r'[A-Z]'))) return 'En az bir buyuk harf olmali';14 if (!value.contains(RegExp(r'[0-9]'))) return 'En az bir rakam olmali';15 return null;16 }17 18 static String? validatePhoneNumber(String? value) {19 if (value == null || value.isEmpty) return 'Telefon gerekli';20 final cleaned = value.replaceAll(RegExp(r'[^0-9+]'), '');21 if (cleaned.length < 10) return 'Gecersiz telefon numarasi';22 return null;23 }24}25 26// test/utils/validators_test.dart27import 'package:flutter_test/flutter_test.dart';28 29void main() {30 group('Validators', () {31 group('validateEmail', () {32 test('bos email icin hata mesaji donmeli', () {33 expect(Validators.validateEmail(''), 'Email gerekli');34 expect(Validators.validateEmail(null), 'Email gerekli');35 });36 37 test('gecersiz format icin hata mesaji donmeli', () {38 expect(Validators.validateEmail('test'), 'Gecersiz email formati');39 expect(Validators.validateEmail('test@'), 'Gecersiz email formati');40 expect(Validators.validateEmail('@test.com'), isNotNull);41 });42 43 test('gecerli email icin null donmeli', () {44 expect(Validators.validateEmail('[email protected]'), isNull);45 expect(Validators.validateEmail('[email protected]'), isNull);46 });47 });48 49 group('validatePassword', () {50 test('kisa sifre icin hata donmeli', () {51 expect(Validators.validatePassword('Ab1'), contains('8 karakter'));52 });53 54 test('buyuk harf olmadan hata donmeli', () {55 expect(Validators.validatePassword('abcdefg1'), contains('buyuk harf'));56 });57 58 test('rakam olmadan hata donmeli', () {59 expect(Validators.validatePassword('Abcdefgh'), contains('rakam'));60 });61 62 test('gecerli sifre icin null donmeli', () {63 expect(Validators.validatePassword('SecurePass1'), isNull);64 });65 });66 });67}Service Katmani Unit Test
dart
1// test/services/cart_service_test.dart2import 'package:flutter_test/flutter_test.dart';3 4class CartService {5 final List _items = []; 6 7 List get items => List.unmodifiable(_items); 8 int get itemCount => _items.length;9 double get totalPrice => _items.fold(0.0, (sum, item) => sum + item.total);10 11 void addItem(Product product, {int quantity = 1}) {12 final existingIndex = _items.indexWhere((i) => i.product.id == product.id);13 if (existingIndex >= 0) {14 _items[existingIndex] = _items[existingIndex].copyWith(15 quantity: _items[existingIndex].quantity + quantity,16 );17 } else {18 _items.add(CartItem(product: product, quantity: quantity));19 }20 }21 22 void removeItem(String productId) {23 _items.removeWhere((item) => item.product.id == productId);24 }25 26 void clear() => _items.clear();27}28 29void main() {30 late CartService cartService;31 late Product testProduct;32 33 setUp(() {34 cartService = CartService();35 testProduct = Product(id: '1', name: 'Test Urun', price: 29.99);36 });37 38 group('CartService', () {39 test('baslangitta bos olmali', () {40 expect(cartService.items, isEmpty);41 expect(cartService.itemCount, 0);42 expect(cartService.totalPrice, 0.0);43 });44 45 test('urun eklendiginde sepet guncellenmeli', () {46 cartService.addItem(testProduct);47 expect(cartService.itemCount, 1);48 expect(cartService.totalPrice, 29.99);49 });50 51 test('ayni urun eklendiginde miktar artmali', () {52 cartService.addItem(testProduct);53 cartService.addItem(testProduct, quantity: 2);54 expect(cartService.itemCount, 1);55 expect(cartService.items.first.quantity, 3);56 expect(cartService.totalPrice, closeTo(89.97, 0.01));57 });58 59 test('urun silindiginde sepetten kalkmali', () {60 cartService.addItem(testProduct);61 cartService.removeItem('1');62 expect(cartService.items, isEmpty);63 });64 65 test('sepet temizlendiginde bos olmali', () {66 cartService.addItem(testProduct);67 cartService.addItem(68 Product(id: '2', name: 'Diger Urun', price: 49.99),69 );70 cartService.clear();71 expect(cartService.items, isEmpty);72 expect(cartService.totalPrice, 0.0);73 });74 });75}Easter Egg
Gizli bir bilgi buldun!
Bu bölümde gizli bir bilgi var. Keşfetmek ister misin?
3. Widget Test
Widget test, tek bir widget'in davranisini ve gorunumunu test eder:
dart
1// lib/widgets/counter_widget.dart2class CounterWidget extends StatefulWidget {3 final int initialValue;4 final void Function(int)? onChanged;5 6 const CounterWidget({7 super.key,8 this.initialValue = 0,9 this.onChanged,10 });11 12 @override13 State createState() => _CounterWidgetState(); 14}15 16class _CounterWidgetState extends State { 17 late int _count;18 19 @override20 void initState() {21 super.initState();22 _count = widget.initialValue;23 }24 25 void _increment() {26 setState(() => _count++);27 widget.onChanged?.call(_count);28 }29 30 void _decrement() {31 if (_count > 0) {32 setState(() => _count--);33 widget.onChanged?.call(_count);34 }35 }36 37 @override38 Widget build(BuildContext context) {39 return Row(40 mainAxisSize: MainAxisSize.min,41 children: [42 IconButton(43 icon: const Icon(Icons.remove),44 onPressed: _count > 0 ? _decrement : null,45 ),46 Text(47 '$_count',48 style: Theme.of(context).textTheme.headlineMedium,49 ),50 IconButton(51 icon: const Icon(Icons.add),52 onPressed: _increment,53 ),54 ],55 );56 }57}58 59// test/widgets/counter_widget_test.dart60import 'package:flutter/material.dart';61import 'package:flutter_test/flutter_test.dart';62 63void main() {64 group('CounterWidget', () {65 testWidgets('baslangic degeri gostermeli', (tester) async {66 await tester.pumpWidget(67 const MaterialApp(68 home: Scaffold(body: CounterWidget(initialValue: 5)),69 ),70 );71 72 expect(find.text('5'), findsOneWidget);73 });74 75 testWidgets('artir butonuna tiklandiginda deger artmali', (tester) async {76 await tester.pumpWidget(77 const MaterialApp(78 home: Scaffold(body: CounterWidget()),79 ),80 );81 82 expect(find.text('0'), findsOneWidget);83 84 await tester.tap(find.byIcon(Icons.add));85 await tester.pump();86 87 expect(find.text('1'), findsOneWidget);88 });89 90 testWidgets('sifirda azalt butonu devre disi olmali', (tester) async {91 await tester.pumpWidget(92 const MaterialApp(93 home: Scaffold(body: CounterWidget()),94 ),95 );96 97 final decrementButton = find.byIcon(Icons.remove);98 final iconButton = tester.widget( 99 find.ancestor(of: decrementButton, matching: find.byType(IconButton)).first,100 );101 102 expect(iconButton.onPressed, isNull);103 });104 105 testWidgets('deger degistiginde callback cagrilmali', (tester) async {106 int? lastValue;107 108 await tester.pumpWidget(109 MaterialApp(110 home: Scaffold(111 body: CounterWidget(112 onChanged: (value) => lastValue = value,113 ),114 ),115 ),116 );117 118 await tester.tap(find.byIcon(Icons.add));119 await tester.pump();120 121 expect(lastValue, 1);122 });123 });124}4. Integration Test
Integration test, uygulamanin tamamini gercek cihazda veya emulator'de test eder:
dart
1// integration_test/app_test.dart2import 'package:flutter_test/flutter_test.dart';3import 'package:integration_test/integration_test.dart';4import 'package:my_app/main.dart' as app;5 6void main() {7 IntegrationTestWidgetsFlutterBinding.ensureInitialized();8 9 group('Uygulama Entegrasyon Testleri', () {10 testWidgets('giris yapip ana sayfayi gormeli', (tester) async {11 app.main();12 await tester.pumpAndSettle();13 14 // Login sayfasinda olmali15 expect(find.text('Giris Yap'), findsOneWidget);16 17 // Email ve sifre gir18 await tester.enterText(19 find.byKey(const Key('emailField')),20 '[email protected]',21 );22 await tester.enterText(23 find.byKey(const Key('passwordField')),24 'SecurePass1',25 );26 27 // Giris butonuna tikla28 await tester.tap(find.byKey(const Key('loginButton')));29 await tester.pumpAndSettle(const Duration(seconds: 3));30 31 // Ana sayfada olmali32 expect(find.text('Hosgeldin'), findsOneWidget);33 });34 });35}bash
1# Integration test calistir2flutter test integration_test/app_test.dart3 4# Belirli cihazda calistir5flutter test integration_test --device-id=emulator-55545. Golden Test
Golden test, widget'larin gorsel ciktisini referans gorsellerle karsilastirir:
dart
1// test/golden/button_golden_test.dart2import 'package:flutter/material.dart';3import 'package:flutter_test/flutter_test.dart';4 5void main() {6 group('CustomButton Golden Tests', () {7 testWidgets('primary buton gorunumu', (tester) async {8 await tester.pumpWidget(9 MaterialApp(10 home: Scaffold(11 body: Center(12 child: CustomButton(13 label: 'Devam Et',14 onPressed: () {},15 ),16 ),17 ),18 ),19 );20 21 await expectLater(22 find.byType(CustomButton),23 matchesGoldenFile('goldens/primary_button.png'),24 );25 });26 });27}bash
1# Golden referans dosyalarini olustur2flutter test --update-goldens3 4# Golden testleri calistir5flutter test test/golden/6. Mocking Stratejileri
dart
1// mockito ile mock olusturma2import 'package:mockito/annotations.dart';3import 'package:mockito/mockito.dart';4 5@GenerateMocks([UserRepository, ApiClient])6void main() {}7 8// Kullanim9void main() {10 late MockUserRepository mockRepo;11 12 setUp(() {13 mockRepo = MockUserRepository();14 });15 16 test('kullanici basariyla getirilmeli', () async {17 final testUser = User(id: '1', name: 'Test');18 19 when(mockRepo.getUser('1')).thenAnswer((_) async => testUser);20 21 final result = await mockRepo.getUser('1');22 23 expect(result, testUser);24 verify(mockRepo.getUser('1')).called(1);25 });26 27 test('hata durumunda exception firlatmali', () async {28 when(mockRepo.getUser('invalid'))29 .thenThrow(Exception('Kullanici bulunamadi'));30 31 expect(32 () => mockRepo.getUser('invalid'),33 throwsException,34 );35 });36}bash
1# Mock siniflarini olustur2dart run build_runner build --delete-conflicting-outputs7. Async Test Kaliplari
dart
1void main() {2 group('Async Testler', () {3 test('Future basari durumu', () async {4 final service = UserService(mockRepo);5 final user = await service.getUser('1');6 expect(user.name, 'Test');7 });8 9 test('Stream birden fazla deger yayinlamali', () async {10 final stream = counterService.countStream(3);11 12 await expectLater(13 stream,14 emitsInOrder([1, 2, 3, emitsDone]),15 );16 });17 18 test('Stream hata durumu', () async {19 final stream = errorService.faultyStream();20 21 await expectLater(22 stream,23 emitsInOrder([1, emitsError(isA()), emitsDone]), 24 );25 });26 27 test('timeout ile zamanlama testi', () async {28 expect(29 service.slowOperation(),30 completes,31 );32 }, timeout: const Timeout(Duration(seconds: 10)));33 });34}8. Test Coverage
bash
1# Coverage raporu olustur2flutter test --coverage3 4# HTML raporu olustur (lcov gerekli)5genhtml coverage/lcov.info -o coverage/html6 7# Tarayicide ac8open coverage/html/index.htmlCoverage Hedefleri
Katman | Hedef | Aciklama |
|---|---|---|
**Domain (UseCase)** | %95+ | Is mantigi kritik |
**Data (Repository)** | %90+ | Veri erisimi guvenilir olmali |
**Utils / Helpers** | %95+ | Yardimci fonksiyonlar kolay test edilir |
**Presentation** | %70+ | Widget testleri ile |
**Toplam Proje** | %80+ | Minimum hedef |
9. CI/CD Entegrasyonu
yaml
1# .github/workflows/flutter-test.yml2name: Flutter Test3on: [push, pull_request]4 5jobs:6 test:7 runs-on: ubuntu-latest8 steps:9 - uses: actions/checkout@v410 - uses: subosito/flutter-action@v211 with:12 flutter-version: '3.24.0'13 channel: 'stable'14 15 - name: Install Dependencies16 run: flutter pub get17 18 - name: Analyze19 run: flutter analyze20 21 - name: Unit ve Widget Testleri22 run: flutter test --coverage23 24 - name: Coverage Kontrol25 run: |26 COVERAGE=$(lcov --summary coverage/lcov.info | grep lines | awk '{print $2}' | sed 's/%//')27 echo "Coverage: $COVERAGE%"28 if (( $(echo "$COVERAGE < 80" | bc -l) )); then29 echo "Coverage %80'in altinda!"30 exit 131 fi10. TDD ile Flutter
Test Driven Development dongusu: Red-Green-Refactor
- Red: Once basan bir test yaz (henuz kod yok, test fail olur)
- Green: Testi gecen minimum kodu yaz
- Refactor: Kodu iyilestir (test hala gecmeli)
dart
1// 1. RED: Test yaz (fail olacak)2test('iki sayi toplandiginda dogru sonuc donmeli', () {3 final calculator = Calculator();4 expect(calculator.add(2, 3), 5);5});6 7// 2. GREEN: Minimum kodu yaz8class Calculator {9 int add(int a, int b) => a + b;10}11 12// 3. REFACTOR: Gerekirse iyilestir13// Bu ornekte refactor gerekli degil, ama karmasik senaryolarda14// kod yapisi, isimlendirme ve performans iyilestirilebilir.11. Best Practices
- AAA Pattern: kullanin: Arrange (hazirla), Act (calistir), Assert (dogrula)
- Her test tek bir seyi test etmeli
- Test isimleri davranisi aciklamali (ne yaptigini degil, ne olmasi gerektigini)
- setUp/tearDown: ile tekrar eden hazirlik kodunu merkezilestirin
- Mock: sadece dis bagimliliklari (network, database, file system)
- Golden test'leri: dikkatli kullanin (CI ortam farki sorun yaratabilir)
Sonuc ve Oneriler
Flutter'da test yazmak, uygulamanizin kalitesini ve guvenilirligini dramatik sekilde arttirir. Dogru test stratejisi ile hem bug sayisini azaltir hem de refactoring cesaretiyle kod kalitenizi surekli yukseltebilirsiniz.
ALTIN İPUCU
Bu yazının en değerli bilgisi
Bu ipucu, yazının en önemli çıkarımını içeriyor.
Okuyucu Ödülü
Tebrikler! Bu yazıyı sonuna kadar okuduğun için sana özel bir hediyem var:

