Tüm Yazılar
KategoriFlutter
Okuma Süresi
25 dk okuma
Yayın Tarihi
...
Kelime Sayısı
1.979kelime

Kahveni hazırla - bu içerikli bir makale!

Flutter uygulamalarinda kapsamli test stratejileri. Unit test, widget test, integration test, golden test, mocking ve CI entegrasyonu.

Flutter Test Rehberi: Unit, Widget ve Integration Test

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


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.dart
2class 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.dart
27import '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.dart
2import '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.dart
2class 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 @override
13 State createState() => _CounterWidgetState();
14}
15 
16class _CounterWidgetState extends State {
17 late int _count;
18 
19 @override
20 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 @override
38 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.dart
60import '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.dart
2import '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 olmali
15 expect(find.text('Giris Yap'), findsOneWidget);
16 
17 // Email ve sifre gir
18 await tester.enterText(
19 find.byKey(const Key('emailField')),
21 );
22 await tester.enterText(
23 find.byKey(const Key('passwordField')),
24 'SecurePass1',
25 );
26 
27 // Giris butonuna tikla
28 await tester.tap(find.byKey(const Key('loginButton')));
29 await tester.pumpAndSettle(const Duration(seconds: 3));
30 
31 // Ana sayfada olmali
32 expect(find.text('Hosgeldin'), findsOneWidget);
33 });
34 });
35}
bash
1# Integration test calistir
2flutter test integration_test/app_test.dart
3 
4# Belirli cihazda calistir
5flutter test integration_test --device-id=emulator-5554

5. Golden Test

Golden test, widget'larin gorsel ciktisini referans gorsellerle karsilastirir:

dart
1// test/golden/button_golden_test.dart
2import '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 olustur
2flutter test --update-goldens
3 
4# Golden testleri calistir
5flutter test test/golden/

6. Mocking Stratejileri

dart
1// mockito ile mock olusturma
2import 'package:mockito/annotations.dart';
3import 'package:mockito/mockito.dart';
4 
5@GenerateMocks([UserRepository, ApiClient])
6void main() {}
7 
8// Kullanim
9void 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 olustur
2dart run build_runner build --delete-conflicting-outputs

7. 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 olustur
2flutter test --coverage
3 
4# HTML raporu olustur (lcov gerekli)
5genhtml coverage/lcov.info -o coverage/html
6 
7# Tarayicide ac
8open coverage/html/index.html

Coverage 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.yml
2name: Flutter Test
3on: [push, pull_request]
4 
5jobs:
6 test:
7 runs-on: ubuntu-latest
8 steps:
9 - uses: actions/checkout@v4
10 - uses: subosito/flutter-action@v2
11 with:
12 flutter-version: '3.24.0'
13 channel: 'stable'
14 
15 - name: Install Dependencies
16 run: flutter pub get
17 
18 - name: Analyze
19 run: flutter analyze
20 
21 - name: Unit ve Widget Testleri
22 run: flutter test --coverage
23 
24 - name: Coverage Kontrol
25 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) )); then
29 echo "Coverage %80'in altinda!"
30 exit 1
31 fi

10. TDD ile Flutter

Test Driven Development dongusu: Red-Green-Refactor

  1. Red: Once basan bir test yaz (henuz kod yok, test fail olur)
  2. Green: Testi gecen minimum kodu yaz
  3. 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 yaz
8class Calculator {
9 int add(int a, int b) => a + b;
10}
11 
12// 3. REFACTOR: Gerekirse iyilestir
13// Bu ornekte refactor gerekli degil, ama karmasik senaryolarda
14// 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:

Etiketler

#Flutter#Testing#Unit Test#Widget Test#Dart#Mockito#CI/CD
Muhittin Çamdalı

Muhittin Çamdalı

Senior iOS Developer

12+ yıllık deneyime sahip iOS Developer. Swift, SwiftUI ve modern iOS mimarileri konusunda uzman. Apple platformlarında performanslı ve kullanıcı dostu uygulamalar geliştiriyorum.

iOS Geliştirme Haberleri

Haftalık Swift tips, SwiftUI tricks ve iOS best practices. Spam yok, sadece değerli içerik.

Gizliliğinize saygı duyuyoruz. İstediğiniz zaman abonelikten çıkabilirsiniz.

Paylaş

Bunu da begenebilirsiniz