টেস্টিং
যেকোনো মাঝারি থেকে বড় আকারের অ্যাপ্লিকেশনের জন্য, অ্যাপ্লিকেশনটি টেস্টিং করা গুরুত্বপূর্ণ।
সফলভাবে আমাদের এ্যাপ টেস্টিং করার জন্য, আমরা নিম্নলিখিত জিনিসগুলি দরকার:
কোন স্টেট
test
/testWidgets
এর মধ্যে সংরক্ষিত থাকবে না।
এটির অর্থ হচ্ছে কোন গ্লোবাল স্টেট থাকবে না, অথবা সকল গ্লোবাল স্টেট রিসেট করতে হবে প্রতি টেস্ট এর পরে।আমাদের প্রভাইডার গুলাকে জোরপূর্বক একটি নির্দিষ্ট স্টেট এ আনার ক্ষমতা, হয়তোবা মকিং দিয়ে অথবা তাদের ম্যানুপুলেট করে যতক্ষণনা আমরা আমদের চাওয়া স্টেট পাচ্ছি না।
চলুন দেখি Riverpod কিভাবে আপনাকে সাহায্য করতে পারে।
কোন স্টেট test
/testWidgets
এর মধ্যে সংরক্ষিত থাকবে না।
সাধারণত প্রোভাইডার গুলা গ্লোবাল ভ্যারিয়েবেল হিসেবে ডিক্লার করা থাকে, যা নিয়ে আপনার চিন্তা করা উচিত।
পরিশেষে, গ্লোবাল স্ট্যাট টেস্ট করা খুব কষ্টসাধ্য একটি কাজ, কারণ এটিতে আপনার অনেক লম্বা setUp
/tearDown
এর মুখোমুখী হতে হবে.
কিন্তু সত্যি এই যে: প্রোভাইডার গুলা গ্লোবাল ভাবে ডিক্লার হলে ও, প্রোভাইডার এর স্ট্যাট গ্লোবাল না।
এর পরিবর্তে, এটি ProviderContainer নাম এর একটি অবজেক্ট এ স্টোর করা থাকে, হয়তো এটি আপনি দেখেছেন, যদি আপনি ডার্ট এর (dart-only) উদাহরণটা দেখে থাকেন।
যদি আপনি না দেখে থাকেন, এটি জেনে রাখেন যে ProviderContainer অবজেক্টটি পরোক্ষভাবে তৈরি হয়ে থাকে ProviderScope দিয়ে, এই উইজেট সেটাই যেটা আমাদের প্রজেক্ট এ Riverpod চালু করে দেই.
নিশ্চিতভাবে এর মানে, দুইটি testWidgets
প্রোভাইডার ব্যবহার করলে ও তাদের মধ্যে কোন স্ট্যাট এর শেয়ার করে না।
এই হিসাবে এখানে কোন setUp
/tearDown
প্রয়োজন নেই একেবারে.
কিন্তু একটি উদাহরণ অনেক উত্তম লম্বা ব্যাখ্যা এর চেয়ে:
- testWidgets (Flutter)
- test (Dart only)
// A Counter implemented and tested using Flutter
// We declared a provider globally, and we will use it in two tests, to see
// if the state correctly resets to `0` between tests.
final counterProvider = StateProvider((ref) => 0);
// Renders the current state and a button that allows incrementing the state
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'),
);
}),
);
}
}
void main() {
testWidgets('update the UI when incrementing the state', (tester) async {
await tester.pumpWidget(ProviderScope(child: MyApp()));
// The default value is `0`, as declared in our provider
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Increment the state and re-render
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// The state have properly incremented
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()));
// The state is `0` once again, with no tearDown/setUp needed
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
}
// A Counter implemented and tested with Dart only (no dependency on Flutter)
// We declared a provider globally, and we will use it in two tests, to see
// if the state correctly resets to `0` between tests.
final counterProvider = StateProvider((ref) => 0);
// Using mockito to keep track of when a provider notify its listeners
class Listener extends Mock {
void call(int? previous, int value);
}
void main() {
test('defaults to 0 and notify listeners when value changes', () {
// An object that will allow us to read providers
// Do not share this between tests.
final container = ProviderContainer();
addTearDown(container.dispose);
final listener = Listener();
// Observe a provider and spy the changes.
container.listen<int>(
counterProvider,
listener.call,
fireImmediately: true,
);
// the listener is called immediately with 0, the default value
verify(listener(null, 0)).called(1);
verifyNoMoreInteractions(listener);
// We increment the value
container.read(counterProvider.notifier).state++;
// The listener was called again, but with 1 this time
verify(listener(0, 1)).called(1);
verifyNoMoreInteractions(listener);
});
test('the counter state is not shared between tests', () {
// We use a different ProviderContainer to read our provider.
// This ensure that no state is reused between tests
final container = ProviderContainer();
addTearDown(container.dispose);
final listener = Listener();
container.listen<int>(
counterProvider,
listener.call,
fireImmediately: true,
);
// The new test correctly uses the default value: 0
verify(listener(null, 0)).called(1);
verifyNoMoreInteractions(listener);
});
}
যেটা আমরা এখানে দেখতে পাচ্ছি, counterProvider
গ্লোবালি ডিক্লার হলেও, টেস্টগুলার মধ্যে কোন স্টেট শেয়ার করা হয়নি। এই কারণে, আমাদের চিন্তা করতে হবে না এই যে, আমাদের টেস্ট গুলা সম্ভবত ভিন্নভাবে বিহেব করবে, যদি আমরা তা ভিন্নভাবে এক্সিকিউট করি, কারণ তা সম্পূর্ণভাবে আইসোলেটেড ভাবে চলতেছে।
একটি প্রভাইডার এর আচরণ ওভারড়াইড করা
একটি সাধারণ বাস্তব-জীবন এর এপ্লিকেশন এর নিম্নোক্ত অবজেক্ট গুলা থাকে:
এটির একটি
Repository
ক্লাস থাকবে, যেটি টাইপ সেইফ এবং সাধারণ একটি এপিয়াই প্রভাইড করে যেটি HTTP রিকুয়েস্ট এক্সিকিউট করেএকটি অবজেক্ট যা এপ্লিকেশন এর স্ট্যট ম্যানেজ করে, এবং এটি হয়তো
Repository
ব্যবহার করবে বিভিন্ন ফ্যাক্টর এর উপর ভিত্তি করে। এটি হয়তোবাChangeNotifier
,Bloc
, অথবা প্রোভাইডার ও হতে পারে.
Riverpod ব্যবহার করে, এটি নিম্নোক্ত ভাবে হয়ে থাকতে পারে:
class Repository {
Future<List<Todo>> fetchTodos() async => [];
}
// We expose our instance of Repository in a provider
final repositoryProvider = Provider((ref) => Repository());
/// The list of todos. Here, we are simply fetching them from the server using
/// [Repository] and doing nothing else.
final todoListProvider = FutureProvider((ref) async {
// Obtains the Repository instance
final repository = ref.watch(repositoryProvider);
// Fetch the todos and expose them to the UI.
return repository.fetchTodos();
});
এই অবস্থায়, যখন আমরা unit/widget টেস্ট বানাব, আমরা সাধারণত আমাদের Repository
ইন্সট্যান্সকে একটি ফেক ইমপ্লেমেন্টেশন দিয়ে রিপ্লেস করে দিব, যেটি আমাদের আগে থেকে ঠিক করা কিছু রেসপন্স পাঠাবে, সত্যিকারের HTTP রিকুয়েস্ট করা ছাড়া।
আমরা তখন আমাদের todoListProvider
কে অথবা তার সমমানকে Repository
এর একটি মক ইমপ্লেমেন্টেশন ব্যবহার করাতে চাইব।
আমরা এটি এচিভ করার জন্য overrides
প্যারামিটারটি আমরা ব্যবহার করতে পারব যা ProviderScope/ProviderContainer এ রয়েছে, repositoryProvider
এর আচরণ পরিবর্তন করার জন্যঃ
- ProviderScope (Flutter)
- ProviderContainer (Dart only)
testWidgets('override repositoryProvider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// Override the behavior of repositoryProvider to return
// FakeRepository instead of Repository.
repositoryProvider.overrideWithValue(FakeRepository())
// We do not have to override `todoListProvider`, it will automatically
// use the overridden repositoryProvider
],
child: MyApp(),
),
);
});
test('override repositoryProvider', () async {
final container = ProviderContainer(
overrides: [
// Override the behavior of repositoryProvider to return
// FakeRepository instead of Repository.
repositoryProvider.overrideWithValue(FakeRepository())
// We do not have to override `todoListProvider`, it will automatically
// use the overridden repositoryProvider
],
);
// The first read if the loading state
expect(
container.read(todoListProvider),
const AsyncValue<List<Todo>>.loading(),
);
/// Wait for the request to finish
await container.read(todoListProvider.future);
// Exposes the data fetched
expect(container.read(todoListProvider).value, [
isA<Todo>()
.having((s) => s.id, 'id', '42')
.having((s) => s.label, 'label', 'Hello world')
.having((s) => s.completed, 'completed', false),
]);
});
হাইলেটেড কোড হতে আপনি দেখতে পাচ্ছেন, ProviderScope/ProviderContainer অনুমতি (Allow) দেই একটি প্রভাইডার এর ইমপ্লেমেন্টেশন এর আচরণ পরিবর্তন করার।
কিছু আর সিম্পল ভাবে ওভাররাইড করার রাস্তা এক্সপোস করে.
উদাহারণসরূপ, FutureProvider অনুমতি দেই AsyncValue
দিয়ে একটি প্রভাইডার ওভাররাইড করার:
final todoListProvider = FutureProvider((ref) async => <Todo>[]);
// ...
/* SKIP */
final foo =
/* SKIP END */
ProviderScope(
overrides: [
/// Allows overriding a FutureProvider to return a fixed value
todoListProvider.overrideWithValue(
AsyncValue.data([Todo(id: '42', label: 'Hello', completed: true)]),
),
],
child: const MyApp(),
);
family
মডিফাইয়ার কে একটি প্রভাইডার দিয়ে ওভাররাইড করার পদ্ধতি একটু ভিন্ন।
যদি আপনি এরকম একটি প্রভাইডার ব্যবহার করে থাকেনঃ
final response = ref.watch(myProvider('12345'));
আপনি প্রভাইডারটা কে এভাবে ওভাররাইড করতে পারেন:
myProvider('12345').overrideWithValue(...));
সম্পূর্ণ উইজেট টেস্ট ঊদাহারণ
এখানেই শেষ করতেছি, নিচে পুরো কোড স্নিপেট দেওয়া হল ফ্লাটার টেস্ট এর।
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;
}
// We expose our instance of Repository in a provider
final repositoryProvider = Provider((ref) => Repository());
/// The list of todos. Here, we are simply fetching them from the server using
/// [Repository] and doing nothing else.
final todoListProvider = FutureProvider((ref) async {
// Obtains the Repository instance
final repository = ref.read(repositoryProvider);
// Fetch the todos and expose them to the UI.
return repository.fetchTodos();
});
/// A mocked implementation of Repository that returns a pre-defined list of todos
class FakeRepository implements Repository {
Future<List<Todo>> fetchTodos() async {
return [
Todo(id: '42', label: 'Hello world', completed: false),
];
}
}
class TodoItem extends StatelessWidget {
const TodoItem({super.key, required this.todo});
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())
],
// Our application, which will read from todoListProvider to display the todo-list.
// You may extract this into a MyApp widget
child: MaterialApp(
home: Scaffold(
body: Consumer(builder: (context, ref, _) {
final todos = ref.watch(todoListProvider);
// The list of todos is loading or in error
if (todos.asData == null) {
return const CircularProgressIndicator();
}
return ListView(
children: [
for (final todo in todos.asData!.value) TodoItem(todo: todo)
],
);
}),
),
),
),
);
// The first frame is a loading state.
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// Re-render. TodoListProvider should have finished fetching the todos by now
await tester.pump();
// No longer loading
expect(find.byType(CircularProgressIndicator), findsNothing);
// Rendered one TodoItem with the data returned by FakeRepository
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),
]);
});
}