Ana içeriğe atla

Testing

Orta ila büyük ölçekli herhangi bir uygulama için uygulamanın test edilmesi önemlidir.

Uygulamamızı başarıyla test etmek için aşağıdakilere ihtiyacımız olacak:

  • test/testWidgets arasında hiçbir durumu korumayın. Bu, uygulamada küresel bir durumun olmadığı veya tüm küresel durumların olmadığı anlamına gelir Her testten sonra sıfırlanmaları gerekir.

  • provider'larımızı belirli bir duruma sahip olmaya zorlayabilecek, ya "alaylar" yoluyla ya da istenen duruma ulaşana kadar onları manipüle ederek.

Riverpod'un bu konuda size nasıl yardımcı olduğunu tek tek görelim.

test/testWidgets arasındaki herhangi bir durumu korumayın.

provider'lar genellikle global değişkenler olarak tanımlandığından, bu konuda endişelenebilirsin. Nihayet, Küresel devlet testleri çok zorlaştırıyor, çünkü oldukça uzun bir 'setUp'/'tearDown' gerektirebilir.

Ancak gerçek şu ki: provider'lar küresel olarak ilan edilirken, bir provider'ın durumu küresel değildir.

Bunun yerine, ProviderContainer adlı bir nesnede saklanır. Solo Dart örneklerini incelerseniz görmüş olabilirsiniz. Henüz yapmadıysanız, bu ProviderContainer nesnesinin örtülü olarak oluşturulduğunu bilin. projemizde Riverpod'u etkinleştiren ProviderScope widget'ı.

Somut olarak bunun anlamı, provider'ları kullanan iki "testWidget"ın Hiçbir statüyü paylaşmıyorlar. Bu nedenle, 'setUp'/'tearDown'a hiçbir şekilde gerek yoktur.

Ancak bir örnek uzun açıklamalardan daha iyidir:

// Flutter kullanılarak uygulanan ve test edilen bir Sayaç

// Global olarak bir provider ilan ediyoruz ve bunu görmek için iki testte kullanacağız.
// testler arasında durum başarılı bir şekilde "0"a sıfırlanırsa.
final counterProvider = StateProvider((ref) => 0);

// Mevcut durumu ve durumu artırmanıza izin veren bir düğmeyi temsil eder
class MyApp extends StatelessWidget {

Widget build(BuildContext context) {
return MaterialApp(
home: Consumer(builder: (context, ref, _) {
final counter = ref.watch(counterProvider);
return ElevatedButton(
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Text('${counter.state}'),
);
}),
);
}
}

void main() {
testWidgets('update the UI when incrementing the state', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));

// provider'ımızın bildirdiği gibi varsayılan değer `0`dır
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);

// Durumu artır ve yeniden oluştur
await tester.tap(find.byType(ElevatedButton));
await tester.pump();

// Durum uygun şekilde arttı
expect(find.text('1'), findsOneWidget);
expect(find.text('0'), findsNothing);
});

testWidgets('the counter state is not shared between tests', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));

// Durum bir kez daha '0' oldu, sökmeye/kurmaya gerek yok
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
}

Gördüğünüz gibi 'counterProvider' global olarak bildirilmiş olmasına rağmen, testler arasında hiçbir durum paylaşılmadı. Bu nedenle, testlerimizin yapılması konusunda endişelenmemize gerek yok farklı bir sırayla yürütülürse farklı davranabilir, tamamen izole bir şekilde yürütüldükleri için.

Testler sırasında provider'ın davranışını geçersiz kılın.

Yaygın bir gerçek dünya uygulaması aşağıdaki nesnelere sahip olabilir:

  • Basit ve güvenli bir API sağlayan bir 'Repository' sınıfı HTTP istekleri yapmak için.

  • Uygulamanın durumunu yöneten ve kullanılabilen bir nesne Farklı faktörlere dayalı olarak HTTP istekleri yapmak için 'Repository'. Bu bir 'ChangeNotifier', 'Bloc' veya hatta bir provider olabilir.

Riverpod kullanılarak bu şu şekilde temsil edilebilir:

class Repository {
Future<List<Todo>> fetchTodos() async {}
}

// Repository örneğimizi bir provider'da kullanıma sunuyoruz
final repositoryProvider = Provider((ref) => Repository());

/// Görev listesi. Burada, bunları kullanarak sunucudan alıyoruz.
/// [Repository], yapacak başka bir şey yok.
final todoListProvider = FutureProvider((ref) async {
// Repository örneğini alın
final repository = ref.read(repositoryProvider);

// Görevleri alın ve bunları kullanıcı arayüzüne gösterin.
return repository.fetchTodos();
});

Bu durumda, bir birim veya widget testi gerçekleştirirken genellikle şunları isteriz: 'Repository' örneğimizi, şunu döndüren sahte bir uygulamayla değiştirin: gerçek bir HTTP isteği yapmak yerine önceden tanımlanmış bir yanıt.

Daha sonra todoListProvider veya eşdeğerimizin 'Repository'nin sahte uygulamasını kullanmasını isteyeceğiz.

Bunu başarmak için ProviderScope/ProviderContainer 'geçersiz kılma' parametresini kullanabiliriz 'repositoryProvider' davranışını geçersiz kılmak için:

testWidgets('override repositoryProvider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Geri dönmek için repositoryProvider'ın davranışını geçersiz kılın
// Repository yerine FakeRepository.
repositoryProvider.overrideWithValue(FakeRepository())
// `todoListProvider`ı geçersiz kılmamıza gerek yok,
// geçersiz kılınan repositoryProvider'ı otomatik olarak kullanacak
],
child: MyApp(),
),
);
}

Vurgulanan kodda görebileceğiniz gibi, ProviderScope/ProviderContainer şunları sağlar: bir provider'ın uygulamasını farklı davranışla değiştirin.

bilgi

Bazı provider'lar davranışlarını geçersiz kılmanın basitleştirilmiş yollarını ortaya koyuyor. Örneğin, FutureProvider, provider'ı bir 'AsyncValue' ile geçersiz kılmanıza olanak tanır:

final todoListProvider = FutureProvider((ref) async => <Todo>[]);
// ...
ProviderScope(
overrides: [
/// Sabit bir değer döndürmek için FutureProvider'ı geçersiz kılmanıza olanak tanır
todoListProvider.overrideWithValue(
AsyncValue.data([Todo(id: '42', label: 'Hello', completed: true)]),
),
],
child: MyApp(),
);
bilgi

Bir provider'ı 'aile' değiştiricisiyle geçersiz kılmanın sözdizimi biraz farklıdır.

Eğer böyle bir provider kullandıysanız:

final response = ref.watch(myProvider('12345'));

provider'ı şu şekilde geçersiz kılabilirsiniz:

myProvider('12345').overrideWithValue(...));

Tam widget testi örneği

Sonuç olarak, Flutter testimizin tam kodunu burada bulabilirsiniz.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';

class Repository {
Future<List<Todo>> fetchTodos() async {}
}

class Todo {
Todo({
required this.id,
required this.label,
required this.completed,
});

final String id;
final String label;
final bool completed;
}

// Repository örneğimizi bir provider'da kullanıma sunuyoruz
final repositoryProvider = Provider((ref) => Repository());

/// Görev listesi. Burada basitçe sunucudan şunu kullanarak alıyoruz:
/// [Repository] ve başka hiçbir şey yapmıyoruz.
final todoListProvider = FutureProvider((ref) async {
// Repository örneğini alın
final repository = ref.read(repositoryProvider);

// Görevleri alın ve bunları kullanıcı arayüzüne gösterin.
return repository.fetchTodos();
});

/// Önceden tanımlanmış bir görev listesi döndüren sahte Repository uygulaması
class FakeRepository implements Repository {

Future<List<Todo>> fetchTodos() async {
return [
Todo(id: '42', label: 'Hello world', completed: false),
];
}
}

class TodoItem extends StatelessWidget {
const TodoItem({Key? key, required this.todo}) : super(key: key);
final Todo todo;

Widget build(BuildContext context) {
return Text(todo.label);
}
}

void main() {
testWidgets('override repositoryProvider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
repositoryProvider.overrideWithValue(FakeRepository())
],
// Yapılacaklar listesini görüntülemek için todoListProvider'dan okuyacak uygulamamız.
// Bunu muhtemelen bir Uygulamam widget'ına çekeceksiniz
child: MaterialApp(
home: Scaffold(
body: Consumer(builder: (context, ref, _) {
final todos = ref.watch(todoListProvider);
// Görev listesi yükleniyor veya hatalı
if (todos.asData == null) {
return const CircularProgressIndicator();
}
return ListView(
children: [
for (final todo in todos.asData!.value) TodoItem(todo: todo)
],
);
}),
),
),
),
);

// İlk çerçeve bir yükleme durumudur.
expect(find.byType(CircularProgressIndicator), findsOneWidget);

// Yeniden oluştur. TodoListProvider'ın tüm görevleri zaten getirmiş olması gerekirdi.
await tester.pump();

// Yükleme durumu yok.
expect(find.byType(CircularProgressIndicator), findsNothing);

// FakeRepository tarafından döndürülen verilerle bir görev oluşturuldu
expect(tester.widgetList(find.byType(TodoItem)), [
isA<TodoItem>()
.having((s) => s.todo.id, 'todo.id', '42')
.having((s) => s.todo.label, 'todo.label', 'Hello world')
.having((s) => s.todo.completed, 'todo.completed', false),
]);
});
}