Flutter Testing Rehberi: Unit, Widget ve Integration Test
# 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.
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.
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.
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.
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.
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.dartTestleri 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.
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.
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),
),
);
}
}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.
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ışı.
# .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.infoBu 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 State Management: Riverpod, Provider ve Bloc Karşılaştırması
Flutter'da state management yaklaşımlarını karşılaştırın. Riverpod, Provider ve Bloc için kullanım senaryolarını ve karar kriterlerini netleştirin.
Flutter'da Clean Architecture: Ölçeklenebilir Uygulama Geliştirme
Flutter projelerinde Clean Architecture'ı uygulanabilir şekilde öğrenin. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için pratik bir rehber.
Flutter CI/CD: Otomatik Build, Test ve Release Süreci
Flutter projelerinde güvenli ve hızlı teslimat için CI/CD pipeline tasarlayın. Build, test ve release adımlarını otomatikleştirin.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç