স্কিপ করে মূল কন্টেন্ট এ যাও

টেস্টিং

যেকোনো মাঝারি থেকে বড় আকারের অ্যাপ্লিকেশনের জন্য, অ্যাপ্লিকেশনটি টেস্টিং করা গুরুত্বপূর্ণ।

সফলভাবে আমাদের এ্যাপ টেস্টিং করার জন্য, আমরা নিম্নলিখিত জিনিসগুলি দরকার:

  • কোন স্টেট test/testWidgets এর মধ্যে সংরক্ষিত থাকবে না।
    এটির অর্থ হচ্ছে কোন গ্লোবাল স্টেট থাকবে না, অথবা সকল গ্লোবাল স্টেট রিসেট করতে হবে প্রতি টেস্ট এর পরে।

  • আমাদের প্রভাইডার গুলাকে জোরপূর্বক একটি নির্দিষ্ট স্টেট এ আনার ক্ষমতা, হয়তোবা মকিং দিয়ে অথবা তাদের ম্যানুপুলেট করে যতক্ষণনা আমরা আমদের চাওয়া স্টেট পাচ্ছি না।

চলুন দেখি Riverpod কিভাবে আপনাকে সাহায্য করতে পারে।

কোন স্টেট test/testWidgets এর মধ্যে সংরক্ষিত থাকবে না।

সাধারণত প্রোভাইডার গুলা গ্লোবাল ভ্যারিয়েবেল হিসেবে ডিক্লার করা থাকে, যা নিয়ে আপনার চিন্তা করা উচিত। পরিশেষে, গ্লোবাল স্ট্যাট টেস্ট করা খুব কষ্টসাধ্য একটি কাজ, কারণ এটিতে আপনার অনেক লম্বা setUp/tearDown এর মুখোমুখী হতে হবে.

কিন্তু সত্যি এই যে: প্রোভাইডার গুলা গ্লোবাল ভাবে ডিক্লার হলে ও, প্রোভাইডার এর স্ট্যাট গ্লোবাল না

এর পরিবর্তে, এটি ProviderContainer নাম এর একটি অবজেক্ট এ স্টোর করা থাকে, হয়তো এটি আপনি দেখেছেন, যদি আপনি ডার্ট এর (dart-only) উদাহরণটা দেখে থাকেন।

যদি আপনি না দেখে থাকেন, এটি জেনে রাখেন যে ProviderContainer অবজেক্টটি পরোক্ষভাবে তৈরি হয়ে থাকে ProviderScope দিয়ে, এই উইজেট সেটাই যেটা আমাদের প্রজেক্ট এ Riverpod চালু করে দেই.

নিশ্চিতভাবে এর মানে, দুইটি testWidgets প্রোভাইডার ব্যবহার করলে ও তাদের মধ্যে কোন স্ট্যাট এর শেয়ার করে না। এই হিসাবে এখানে কোন setUp/tearDown প্রয়োজন নেই একেবারে.

কিন্তু একটি উদাহরণ অনেক উত্তম লম্বা ব্যাখ্যা এর চেয়ে:


// একটি কাউন্টার ইমপ্লিমেন্ট এবং টেস্টেড ফ্লাটার ব্যবহার করে

// আমরা প্রভাইডার গ্লোবালি ডিক্লার করেছি, এবং আমরা এটি দুইটি টেস্ট এ ব্যবহার করব, এটি দেখার জন্য যে,
// স্টেটটা সঠিকভাবে '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);
});
}

যেটা আমরা এখানে দেখতে পাচ্ছি, 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 এর আচরণ পরিবর্তন করার জন্যঃ


testWidgets('override repositoryProvider', (tester) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
// repositoryProvider এর আচরণ পরিবর্তন করে
// FakeRepository রিটার্ন করবে আসল Repository এর বদলে
repositoryProvider.overrideWithValue(FakeRepository())
// আমাদের `todoListProvider` প্রভাইডার ওভাররাইড করার প্রয়োজন নেই,
// এটি অটোমেটিকলী ওভাররাইডেন repositoryProvider ব্যবহার করবে
],
child: MyApp(),
),
);
});

হাইলেটেড কোড হতে আপনি দেখতে পাচ্ছেন, ProviderScope/ProviderContainer অনুমতি (Allow) দেই একটি প্রভাইডার এর ইমপ্লেমেন্টেশন এর আচরণ পরিবর্তন করার।

info

কিছু আর সিম্পল ভাবে ওভাররাইড করার রাস্তা এক্সপোস করে.
উদাহারণসরূপ, 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(),
);
info

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),
]);
});
}