টেস্টিং
যেকোনো মাঝারি থেকে বড় আকারের অ্যাপ্লিকেশনের জন্য, অ্যাপ্লিকেশনটি টেস্টিং করা গুরুত্বপূর্ণ।
সফলভাবে আমাদের এ্যাপ টেস্টিং করার জন্য, আমরা নিম্নলিখিত জিনিসগুলি দরকার:
কোন স্টেট
test
/testWidgets
এর মধ্যে সংরক্ষিত থাকবে না।
এটির অর্থ হচ্ছে কোন গ্লোবাল স্টেট থাকবে না, অথবা সকল গ্লোবাল স্টেট রিসেট করতে হবে প্রতি টেস্ট এর পরে।আমাদের প্রভাইডার গুলাকে জোরপূর্বক একটি নির্দিষ্ট স্টেট এ আনার ক্ষমতা, হয়তোবা মকিং দিয়ে অথবা তাদের ম্যানুপুলেট করে যতক্ষণনা আমরা আমদের চাওয়া স্টেট পাচ্ছি না।
চলুন দেখি Riverpod কিভাবে আপনাকে সাহায্য করতে পারে।
কোন স্টেট test
/testWidgets
এর মধ্যে সংরক্ষিত থাকবে না।
সাধারণত প্রোভাইডার গুলা গ্লোবাল ভ্যারিয়েবেল হিসেবে ডিক্লার করা থাকে, যা নিয়ে আপনার চিন্তা করা উচিত।
পরিশেষে, গ্লোবাল স্ট্যাট টেস্ট করা খুব কষ্টসাধ্য একটি কাজ, কারণ এটিতে আপনার অনেক লম্বা setUp
/tearDown
এর মুখোমুখী হতে হবে.
কিন্তু সত্যি এই যে: প্রোভাইডার গুলা গ্লোবাল ভাবে ডিক্লার হলে ও, প্রোভাইডার এর স্ট্যাট গ্লোবাল না।
এর পরিবর্তে, এটি ProviderContainer নাম এর একটি অবজেক্ট এ স্টোর করা থাকে, হয়তো এটি আপনি দেখেছেন, যদি আপনি ডার্ট এর (dart-only) উদাহরণটা দেখে থাকেন।
যদি আপনি না দেখে থাকেন, এটি জেনে রাখেন যে ProviderContainer অবজেক্টটি পরোক্ষভাবে তৈরি হয়ে থাকে ProviderScope দিয়ে, এই উইজেট সেটাই যেটা আমাদের প্রজেক্ট এ Riverpod চালু করে দেই.
নিশ্চিতভাবে এর মানে, দুইটি testWidgets
প্রোভাইডার ব্যবহার করলে ও তাদের মধ্যে কোন স্ট্যাট এর শেয়ার করে না।
এই হিসাবে এখানে কোন setUp
/tearDown
প্রয়োজন নেই একেবারে.
কিন্তু একটি উদাহরণ অনেক উত্তম লম্বা ব্যাখ্যা এর চেয়ে:
- testWidgets (Flutter)
- test (Dart only)
// একটি কাউন্টার ইমপ্লিমেন্ট এবং টেস্টেড ফ্লাটার ব্যবহার করে
// আমরা প্রভাইডার গ্লোবালি ডিক্লার করেছি, এবং আমরা এটি দুইটি টেস্ট এ ব্যবহার করব, এটি দেখার জন্য যে,
// স্টেটটা সঠিকভাবে '0' তে রিসেট হই কিনা, টেস্টগুলার মধ্যে
final counterProvider = StateProvider((ref) => 0);
// বর্তমান স্টেট রেন্ডার করে এবং একটি বাটন যেটি স্টেট বাড়ানোতে সহায়তা করে
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()));
// ডিফল্ট মান হল `0`, যেটি প্রভাইডার দ্বারা ডিক্লার হয়েছে
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// স্টেট বাড়ান এবং আবার রেনডার করা
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// স্টেট সঠিকভাবে বেড়েছে
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()));
// স্টেটকে আবার `0` করা হল, কিন্তু কোন tearDown/setUp ছাড়াই
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
});
}
// একটি কাউন্টার ইমপ্লিমেন্ট এবং টেস্টেড শুধুমাত্র ডার্ট ব্যবহার করে (ফ্লাটার এর উপর নির্ভর না হয়ে)
// আমরা প্রভাইডার গ্লোবালি ডিক্লার করেছি, এবং আমরা এটি দুইটি টেস্ট এ ব্যবহার করব, এটি দেখার জন্য যে,
// স্টেটটা সঠিকভাবে '0' তে রিসেট হই কিনা, টেস্টগুলার মধ্যে
final counterProvider = StateProvider((ref) => 0);
// এখানে mockito ব্যবহার করা হচ্ছে, এটি ট্র্যাক করার জন্য যে কখন একটি প্রভাইডার
// লিসেনার গুলা কে নটিফাই করে
class Listener extends Mock {
void call(int? previous, int value);
}
void main() {
test('defaults to 0 and notify listeners when value changes', () {
// একটি অবজেক্ট যা আমাদের প্রভাডার রিড করতে সাহায্য করবে
// টেস্ট এর মাঝে এটি কখনো শেয়ার করবেন না.
final container = ProviderContainer();
addTearDown(container.dispose);
final listener = Listener();
// একটি প্রভাইডার অবসারব করে এবং পরির্বতন গুলা গোয়েন্দাগিরি করে
container.listen<int>(
counterProvider,
listener.call,
fireImmediately: true,
);
// এই লিসেনারটা সাথে সাথে কল হবে 0 এর সাথে, যেটি ডিফল্ট ভ্যালু
verify(listener(null, 0)).called(1);
verifyNoMoreInteractions(listener);
// আমরা ভ্যালু টা বাড়ালাম
container.read(counterProvider.notifier).state++;
// লিসেনার আবার কল করা গেল তবে এবার '1' এর সাথে
verify(listener(0, 1)).called(1);
verifyNoMoreInteractions(listener);
});
test('the counter state is not shared between tests', () {
// আমরা এবার এখানে ভিন্ন ধরনের ProviderContainer ব্যবহার করব, প্রভাইডার রিড করার জন্য
// এটি আমাদের নিশ্চয়তা দেই যে, এখানে টেস্ট এর মধ্যে অন্য কোন স্টেট ব্যবহার হচ্ছে না
final container = ProviderContainer();
addTearDown(container.dispose);
final listener = Listener();
container.listen<int>(
counterProvider,
listener.call,
fireImmediately: true,
);
// নতুন টেস্ট সঠিকভাবে ডিফল্ট ভ্যালুটি ব্যবহার করে: 0
verify(listener(null, 0)).called(1);
verifyNoMoreInteractions(listener);
});
}
যেটা আমরা এখানে দেখতে পাচ্ছি, counterProvider
গ্লোবালি ডিক্লার হলেও, টেস্টগুলার মধ্যে কোন স্টেট শেয়ার করা হয়নি। এই কারণে, আমাদের চিন্তা করতে হবে না এই যে, আমাদের টেস্ট গুলা সম্ভবত ভিন্নভাবে বিহেব করবে, যদি আমরা তা ভিন্নভাবে এক্সিকিউট করি, কারণ তা সম্পূর্ণভাবে আইসোলেটেড ভাবে চলতেছে।
একটি প্রভাইডার এর আচরণ ওভারড়াইড করা
একটি সাধারণ বাস্তব-জীবন এর এপ্লিকেশন এর নিম্নোক্ত অবজেক্ট গুলা থাকে:
এটির একটি
Repository
ক্লাস থাকবে, যেটি টাইপ সেইফ এবং সাধারণ একটি এপিয়াই প্রভাইড করে যেটি HTTP রিকুয়েস্ট এক্সিকিউট করেএকটি অবজেক্ট যা এপ্লিকেশন এর স্ট্যট ম্যানেজ করে, এবং এটি হয়তো
Repository
ব্যবহার করবে বিভিন্ন ফ্যাক্টর এর উপর ভিত্তি করে। এটি হয়তোবাChangeNotifier
,Bloc
, অথবা প্রোভাইডার ও হতে পারে.
Riverpod ব্যবহার করে, এটি নিম্নোক্ত ভাবে হয়ে থাকতে পারে:
class Repository {
Future<List<Todo>> fetchTodos() async => [];
}
// আমরা আমাদের রিপোসেটোরি এর ইন্সটেন্স-টি একটি প্রভাইডার এ এক্সপোস করলাম
final repositoryProvider = Provider((ref) => Repository());
/// টুডুস গুলার তালিকা. আমরা এখানে এটি খুব সাধারণ ভাবে সার্ভার থেকে ফেচ করতেছি
/// [Repository] ব্যবহার করে এবং এখানে আমরা আর কিছু করতেছি না।
final todoListProvider = FutureProvider((ref) async {
// রিপোসেটোরি এর ইন্সট্যান্স নিলাম
final repository = ref.watch(repositoryProvider);
// টুডু গুলা ফেচ করলাম, এবং তা ইউয়াই (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: [
// repositoryProvider এর আচরণ পরিবর্তন করে
// FakeRepository রিটার্ন করবে আসল Repository এর বদলে
repositoryProvider.overrideWithValue(FakeRepository())
// আমাদের `todoListProvider` প্রভাইডার ওভাররাইড করার প্রয়োজন নেই,
// এটি অটোমেটিকলী ওভাররাইডেন repositoryProvider ব্যবহার করবে
],
child: MyApp(),
),
);
});
test('override repositoryProvider', () async {
final container = ProviderContainer(
overrides: [
// repositoryProvider এর আচরণ পরিবর্তন করে
// FakeRepository রিটার্ন করবে আসল Repository এর বদলে
repositoryProvider.overrideWithValue(FakeRepository())
// আমাদের `todoListProvider` প্রভাইডার ওভাররাইড করার প্রয়োজন নেই,
// এটি অটোমেটিকলী ওভাররাইডেন repositoryProvider ব্যবহার করবে
],
);
// প্রথম রিড, লোডিং স্ট্যাটাস কিনা চেক হচ্ছে
expect(
container.read(todoListProvider),
const AsyncValue<List<Todo>>.loading(),
);
/// রিকুয়েস্ট শেষ হওয়ার জন্য অপেক্ষা চলতেছে
await container.read(todoListProvider.future);
// ফেচ হওয়া ডাটা এক্সপোস করবে
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
/// একটি ফিক্সড ভ্যালু রিটার্ন করার অনুমতি আছে FutureProvider ওভাররাইড করে
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;
}
// আমরা আমাদের ইন্সটেন্স একটি প্রভাইডার এ এক্সপোস করলাম
final repositoryProvider = Provider((ref) => Repository());
/// টুডুস গুলার তালিকা. আমরা এখানে এটি খুব সাধারণ ভাবে সার্ভার থেকে ফেচ করতেছি
/// [Repository] ব্যবহার করে এবং এখানে আমরা আর কিছু করতেছি না।
final todoListProvider = FutureProvider((ref) async {
// রিপোসেটোরি এর ইন্সট্যান্স নিলাম
final repository = ref.read(repositoryProvider);
// টুডু গুলা ফেচ করলাম, এবং তা ইউয়াই (UI) তে এক্সপোস করে দিলাম
return repository.fetchTodos();
});
/// একটি রিপোসোটোরী এর মকেড ইমপ্লেমেন্টেশন যেটি একটি পূর্ব নির্ধারিত টুডু লিস্ট রিটার্ন করে
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())],
// আমাদের এপ্লিকেশন, যেটি todoListProvider থেকে রিড করবে todo-list ডিসপ্লে করার জন্যে
// আপনি এটি এক্সট্রেক্ট অথবা রিফেক্টর করে MyApp উইজেট করতে পারেন
child: MaterialApp(
home: Scaffold(
body: Consumer(builder: (context, ref, _) {
final todos = ref.watch(todoListProvider);
// টুডুস গুলার তালিকা, এটা কি Error না কি Loading এ আছে
if (todos.asData == null) {
return const CircularProgressIndicator();
}
return ListView(
children: [
for (final todo in todos.asData!.value) TodoItem(todo: todo)
],
);
}),
),
),
),
);
// প্রথম ফ্রেমটি হচ্ছে লোডিং স্ট্যাট (Loading)
expect(find.byType(CircularProgressIndicator), findsOneWidget);
// রি-রেন্ডার. TodoListProvider এতক্ষণে টুডুস গুলা ফেচ করার কাজ শেষ করে ফেলেছে
await tester.pump();
// লোডিং আর থাকবে না
expect(find.byType(CircularProgressIndicator), findsNothing);
// একটি TodoItem রেন্ডার হয়েছে, যে ডাটা গুলা 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),
]);
});
}