Flutter'da Test Stratejileri: Unit, Widget ve Integration Tests
Flutter test piramidi üç katmandan oluşur: en hızlı ve en çok yazılan unit testler, widget render ve etkileşimini doğrulayan widget testler ve gerçek cihaz/emülatörde uçtan uca çalışan integration testler. Her katmanın farklı bir maliyeti ve güveni vardır.
Unit Test: İş Mantığını Test Etmek
Bağımlılıkları mocklamak için mockito ve build_runner kullanın:
// pubspec.yaml
// dev_dependencies:
// flutter_test:
// sdk: flutter
// mockito: ^5.4.4
// build_runner: ^2.4.8
// test/services/cart_service_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:my_app/repositories/product_repository.dart';
import 'package:my_app/services/cart_service.dart';
@GenerateMocks([ProductRepository])
import 'cart_service_test.mocks.dart';
void main() {
late CartService cartService;
late MockProductRepository mockRepo;
setUp(() {
mockRepo = MockProductRepository();
cartService = CartService(repository: mockRepo);
});
group('CartService', () {
test('sepete ürün ekleme toplam fiyatı günceller', () {
// Arrange
final product = Product(id: '1', name: 'Laptop', price: 25000);
when(mockRepo.getById('1')).thenReturn(product);
// Act
cartService.addItem('1', quantity: 2);
// Assert
expect(cartService.totalPrice, equals(50000));
expect(cartService.itemCount, equals(2));
verify(mockRepo.getById('1')).called(1);
});
test('stok yetersizse hata fırlatır', () {
when(mockRepo.getStock('1')).thenReturn(3);
expect(
() => cartService.addItem('1', quantity: 5),
throwsA(isA<InsufficientStockException>()),
);
});
test('aynı ürün tekrar eklenince miktar artar', () {
final product = Product(id: '1', name: 'Kalem', price: 10);
when(mockRepo.getById('1')).thenReturn(product);
when(mockRepo.getStock('1')).thenReturn(100);
cartService.addItem('1', quantity: 1);
cartService.addItem('1', quantity: 1);
expect(cartService.getQuantity('1'), equals(2));
expect(cartService.items.length, equals(1));
});
});
}
// Async test
test('ürün listesi uzaktan yüklenir', () async {
when(mockRepo.fetchProducts()).thenAnswer(
(_) async => [
Product(id: '1', name: 'A', price: 100),
Product(id: '2', name: 'B', price: 200),
],
);
final products = await cartService.loadProducts();
expect(products.length, equals(2));
});
Cubit/BLoC Unit Testi
// pubspec: bloc_test: ^9.1.7
import 'package:bloc_test/bloc_test.dart';
void main() {
group('LoginCubit', () {
late LoginCubit cubit;
late MockAuthRepository mockAuth;
setUp(() {
mockAuth = MockAuthRepository();
cubit = LoginCubit(authRepository: mockAuth);
});
tearDown(() => cubit.close());
blocTest<LoginCubit, LoginState>(
'geçerli kimlik bilgileriyle giriş başarılı',
build: () {
when(mockAuth.login(email: 'test@test.com', password: '123456'))
.thenAnswer((_) async => User(id: '1', email: 'test@test.com'));
return cubit;
},
act: (c) => c.login(email: 'test@test.com', password: '123456'),
expect: () => [
const LoginState(status: LoginStatus.loading),
LoginState(status: LoginStatus.success,
user: User(id: '1', email: 'test@test.com')),
],
);
blocTest<LoginCubit, LoginState>(
'hatalı şifrede hata mesajı döner',
build: () {
when(mockAuth.login(email: anyNamed('email'), password: anyNamed('password')))
.thenThrow(const AuthException('Geçersiz şifre'));
return cubit;
},
act: (c) => c.login(email: 'a@b.com', password: 'yanlis'),
expect: () => [
const LoginState(status: LoginStatus.loading),
const LoginState(status: LoginStatus.error, error: 'Geçersiz şifre'),
],
);
});
}
Widget Test: UI Render ve Etkileşim
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mockito/mockito.dart';
void main() {
group('LoginPage widget testi', () {
testWidgets('boş form gönderildiğinde hata mesajları gösterilir',
(tester) async {
await tester.pumpWidget(
MaterialApp(
home: BlocProvider<LoginCubit>(
create: (_) => LoginCubit(authRepository: MockAuthRepository()),
child: const LoginPage(),
),
),
);
// Form elemanlarının varlığını kontrol et
expect(find.byType(TextFormField), findsNWidgets(2));
expect(find.text('Giriş Yap'), findsOneWidget);
// Butona tıkla
await tester.tap(find.widgetWithText(ElevatedButton, 'Giriş Yap'));
await tester.pump(); // bir frame bekle
// Doğrulama hatalarını kontrol et
expect(find.text('E-posta zorunludur'), findsOneWidget);
expect(find.text('Şifre zorunludur'), findsOneWidget);
});
testWidgets('geçerli form gönderildiğinde loading gösterilir',
(tester) async {
final mockRepo = MockAuthRepository();
when(mockRepo.login(email: anyNamed('email'), password: anyNamed('password')))
.thenAnswer((_) => Future.delayed(const Duration(seconds: 1),
() => User(id: '1', email: 'test@test.com')));
await tester.pumpWidget(
MaterialApp(
home: BlocProvider(
create: (_) => LoginCubit(authRepository: mockRepo),
child: const LoginPage(),
),
),
);
// Form doldur
await tester.enterText(
find.byKey(const Key('email_field')), 'test@test.com');
await tester.enterText(
find.byKey(const Key('password_field')), '123456');
await tester.tap(find.widgetWithText(ElevatedButton, 'Giriş Yap'));
await tester.pump(); // state değişimini işle
expect(find.byType(CircularProgressIndicator), findsOneWidget);
});
});
}
Golden Test: UI Görsel Regresyon
Widget'ın görsel çıktısını bir referans dosyasıyla karşılaştırır. UI değişiklikleri kasıtsız olarak kırılmasın diye kullanılır:
testWidgets('ProductCard altın görüntüyle eşleşir', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: AppTheme.light,
home: Scaffold(
body: ProductCard(
product: Product(
id: '1',
title: 'Test Ürün',
price: 299.99,
imageUrl: 'https://via.placeholder.com/150',
),
),
),
),
);
await expectLater(
find.byType(ProductCard),
matchesGoldenFile('goldens/product_card.png'),
);
});
// İlk çalıştırma: flutter test --update-goldens
// Sonraki çalıştırmalar: görsel farklılık varsa test başarısız olur
Integration Test: Uçtan Uca
// integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Giriş akışı entegrasyon testi', () {
testWidgets('kullanıcı giriş yapıp ana sayfaya ulaşır', (tester) async {
app.main();
await tester.pumpAndSettle();
// Giriş sayfasındayız
expect(find.text('Hoş Geldiniz'), findsOneWidget);
await tester.enterText(find.byKey(const Key('email_field')),
'demo@example.com');
await tester.enterText(find.byKey(const Key('password_field')),
'Demo1234!');
await tester.tap(find.widgetWithText(ElevatedButton, 'Giriş Yap'));
await tester.pumpAndSettle(const Duration(seconds: 3));
// Ana sayfadayız
expect(find.text('Ana Sayfa'), findsOneWidget);
expect(find.byType(BottomNavigationBar), findsOneWidget);
});
});
}
// Çalıştırma: flutter test integration_test/ -d emulator-5554
İyi test stratejisi demek çok sayıda unit test, daha az widget testi ve kritik akışlar için birkaç integration testi demektir. Tüm testleri CI/CD pipeline'a entegre ederek her push'ta otomatik çalıştırın.