أساسيات اختبار الوحدات
أساسيات اختبار الوحدات
اختبار الوحدات هو ممارسة التحقق من أن الدوال والفئات الفردية تتصرف بالضبط كما هو متوقع بشكل منعزل. في Dart وFlutter، تعمل اختبارات الوحدات على Dart VM دون الحاجة إلى جهاز أو محاكي، مما يجعلها أسرع فئة من الاختبارات في هرم الاختبارات. تمنحك مجموعة اختبارات وحدات مكتوبة بشكل صحيح الثقة لإعادة هيكلة الكود وإضافة ميزات دون إدخال تراجعات.
حزمة test هي الأساس لجميع الاختبارات في نظام Dart البيئي. وهي تبعية مباشرة لـ flutter_test، مما يعني أنها متاحة بالفعل في كل مشروع Flutter. أضفها صراحةً تحت dev_dependencies إذا كنت تكتب حزم Dart نقية:
pubspec.yaml — إضافة حزمة test
dev_dependencies:
flutter_test:
sdk: flutter
test: ^1.24.0 # لحزم Dart النقية خارج Flutter
دالتا test() و expect()
كل اختبار وحدة يُعرَّف باستخدام الدالة العامة test(). تقبل سلسلة وصف ورد نداء يحتوي على تأكيداتك. داخل رد النداء تستخدم expect() لمقارنة القيمة الفعلية التي ينتجها كودك مع مطابق يصف النتيجة المتوقعة.
الاستخدام الأساسي لـ test() و expect()
import 'package:test/test.dart';
// دالة Dart النقية قيد الاختبار
int add(int a, int b) => a + b;
String greet(String name) => 'Hello, $name!';
void main() {
test('add ترجع مجموع عددين صحيحين', () {
expect(add(2, 3), equals(5));
expect(add(-1, 1), equals(0));
expect(add(0, 0), equals(0));
});
test('greet ترجع تحية شخصية', () {
expect(greet('Edrees'), equals('Hello, Edrees!'));
expect(greet(''), equals('Hello, !'));
});
}
test/ وأن تنتهي أسماؤها بـ _test.dart. شغّلها باستخدام flutter test (مشاريع Flutter) أو dart test (حزم Dart النقية).تنظيم الاختبارات باستخدام group()
عندما يكون لديك اختبارات كثيرة لنفس الفئة أو الوحدة، فإن تغليفها في كتلة group() يحسّن قابلية القراءة ويجعل رسائل الفشل سياقية. يمكن أيضاً تداخل المجموعات.
تجميع الاختبارات المتعلقة
import 'package:test/test.dart';
class Calculator {
int add(int a, int b) => a + b;
int subtract(int a, int b) => a - b;
double divide(int a, int b) {
if (b == 0) throw ArgumentError('Cannot divide by zero');
return a / b;
}
}
void main() {
group('Calculator', () {
late Calculator calc;
setUp(() {
calc = Calculator(); // نسخة جديدة قبل كل اختبار
});
group('add', () {
test('يرجع المجموع الصحيح للأعداد الموجبة', () {
expect(calc.add(3, 4), equals(7));
});
test('يعالج المعاملات السالبة', () {
expect(calc.add(-5, 3), equals(-2));
});
});
group('divide', () {
test('يرجع ناتج قسمة من نوع double', () {
expect(calc.divide(10, 4), equals(2.5));
});
test('يطرح ArgumentError عند القسمة على صفر', () {
expect(() => calc.divide(5, 0), throwsA(isA<ArgumentError>()));
});
});
});
}
setUp() و tearDown()
تُشغَّل setUp() ردَّ ندائها قبل كل اختبار في النطاق الحالي. تُشغَّل tearDown() ردَّ ندائها بعد كل اختبار، حتى لو أطلق الاختبار استثناءً. استخدمهما لإنشاء بيانات اختبار جديدة وتحرير الموارد (اتصالات قاعدة البيانات، مقابض الملفات، كائنات المحاكاة) حتى تبقى الاختبارات مستقلة ولا تتسرب الحالة بينها.
setUpAll()— تعمل مرة واحدة قبل المجموعة بأكملها (إعداد مكلف كتشغيل خادم)tearDownAll()— تعمل مرة واحدة بعد المجموعة بأكملها (إيقاف ذلك الخادم)setUp()— تعمل قبل كل اختبار (إنشاء نسخة كائن نظيفة)tearDown()— تعمل بعد كل اختبار (إغلاق التدفقات، إعادة تعيين المفردات)
late على مستوى المجموعة وابدأها داخل setUp(). هذا يضمن أن كل اختبار يبدأ بنسخة نظيفة ويمنع أخطاء الترتيب الخفية.المطابقات الشائعة
تأتي حزمة test مع مجموعة غنية من المطابقات المدمجة التي تجعل فشل التأكيدات موثقاً ذاتياً:
equals(value)— تساوٍ عميق (استخدم للبدائيات والقوائم والخرائط)isTrue/isFalse— فحوصات منطقيةisNull/isNotNullisA<Type>()— فحص النوع (isA<String>())throwsA(matcher)— يتحقق من أن دالة تطرح استثناءً مطابقاًthrowsArgumentError،throwsStateError— اختصارات مريحةcontains(element)— يفحص أن قائمة أو سلسلة أو خريطة تحتوي على قيمةhasLength(n)— يفحص طول مجموعة أو سلسلة
المطابقات في التطبيق
void main() {
test('أمثلة على المطابقات', () {
final items = <String>['apple', 'banana', 'cherry'];
expect(items, hasLength(3));
expect(items, contains('banana'));
expect(items.first, isA<String>());
expect(items.isEmpty, isFalse);
String? maybeNull;
expect(maybeNull, isNull);
expect(
() => int.parse('not a number'),
throwsA(isA<FormatException>()),
);
});
}
اختبار الكود غير المتزامن
Dart غير متزامن في جوهره. ضع علامة async على رد نداء الاختبار واستخدم await تماماً كما تفعل في كود الإنتاج. يتعامل مشغّل الاختبار مع Future تلقائياً.
اختبار دالة ترجع Future
import 'package:test/test.dart';
Future<String> fetchUsername(int id) async {
// محاكاة جلب بيانات غير متزامن
await Future.delayed(const Duration(milliseconds: 10));
if (id <= 0) throw ArgumentError('id must be positive');
return 'user_$id';
}
void main() {
test('fetchUsername ترجع اسم مستخدم منسقاً', () async {
final result = await fetchUsername(42);
expect(result, equals('user_42'));
});
test('fetchUsername تطرح استثناءً لمعرف غير موجب', () async {
expect(
() async => fetchUsername(-1),
throwsA(isA<ArgumentError>()),
);
});
}
async/await في الاختبارات غير المتزامنة. رد نداء الاختبار الذي يرجع Future دون أن يكون مُعلَّماً بـ async قد يبدو أنه نجح في حين أن التأكيد لم يُشغَّل فعلياً قط.الخلاصة
اختبارات الوحدات هي العمود الفقري لقاعدة كود Dart وFlutter الموثوقة. تذكر هذه النقاط الرئيسية:
- استخدم
test()لتعريف اختبار وexpect()للتأكيد على النتائج باستخدام مطابقات وصفية. - استخدم
group()لتنظيم الاختبارات منطقياً وإبقاء مخرجات الفشل سياقية. - استخدم
setUp()وtearDown()للتحضير وتنظيف الحالة حول كل اختبار لضمان العزل الكامل. - ضع علامة
asyncعلى ردود نداء الاختبار عند اختبار الكود الذي يرجعFuture. - شغّل جميع الاختبارات باستخدام
flutter testواسعَ إلى اختبارات سريعة وحتمية وخالية من الآثار الجانبية.