اختبار الفئات والحالة والحالات الحدية
اختبار الفئات والحالة والحالات الحدية
كتابة الاختبارات للدوال الفردية تُعدّ بداية جيدة، غير أن التطبيقات الحقيقية مبنية من فئات تحتفظ بحالة وتكشف عن أساليب تتفاعل مع بعضها. في هذا الدرس ستتعلم كيفية استخدام group() لتنظيم الاختبارات ذات الصلة، وكيفية اختبار فئات Dart ذات الحالة بشكل شامل، وكيفية تغطية الحالات الحدية بصورة منهجية — كالمدخلات الفارغة، والقيم الحدية، ومسارات الأخطاء — التي كثيراً ما تتسلل منها الأخطاء البرمجية.
تنظيم الاختبارات باستخدام group()
تتيح لك الدالة group() من حزمة flutter_test / test تجميع الاختبارات المترابطة تحت وصف مشترك. يمكن تداخل المجموعات داخل مجموعات أخرى، ولكل مجموعة دوالها الخاصة setUp() وtearDown() التي تُنفَّذ قبل كل اختبار وبعده داخل تلك المجموعة.
استخدام group() لهيكلة ملف الاختبار
import 'package:test/test.dart';
import 'package:my_app/models/cart.dart';
void main() {
group('Cart', () {
late Cart cart;
setUp(() {
// يُنفَّذ قبل كل اختبار في هذه المجموعة
cart = Cart();
});
group('addItem', () {
test('increases item count by 1', () {
cart.addItem(Item(id: '1', price: 9.99));
expect(cart.itemCount, equals(1));
});
test('accumulates total price correctly', () {
cart.addItem(Item(id: '1', price: 9.99));
cart.addItem(Item(id: '2', price: 4.50));
expect(cart.total, closeTo(14.49, 0.001));
});
});
group('removeItem', () {
test('decreases item count by 1', () {
cart.addItem(Item(id: '1', price: 9.99));
cart.removeItem('1');
expect(cart.itemCount, equals(0));
});
test('throws StateError when item is not in cart', () {
expect(
() => cart.removeItem('nonexistent'),
throwsA(isA<StateError>()),
);
});
});
});
}
setUp() ينشئ نسخة جديدة من Cart، مما يمنع أي اختبار من تلويث الحالة التي يراها اختبار آخر. احرص دائماً على إعادة تهيئة الكائنات القابلة للتغيير في setUp() بدلاً من أعلى الملف.اختبار الفئات ذات الحالة
الفئة ذات الحالة تحتفظ ببيانات داخلية تتغير مع استدعاء أساليبها. لاختبارها بموثوقية تحتاج إلى:
- التحقق من الحالة الأولية مباشرةً بعد الإنشاء.
- اختبار كل انتقال: استدعاء أسلوب والتحقق من الحالة الناتجة.
- اختبار سلاسل الاستدعاء لاكتشاف التفاعلات بين الأساليب.
- التأكد من حدوث الآثار الجانبية كالأحداث المُطلَقة والمستمعين المُنبَّهين والتدفقات المُحدَّثة.
اختبار فئة Counter ذات الحالة
// الفئة قيد الاختبار
class Counter {
int _value;
final int min;
final int max;
Counter({this.min = 0, this.max = 10}) : _value = min;
int get value => _value;
void increment() {
if (_value < max) _value++;
}
void decrement() {
if (_value > min) _value--;
}
void reset() => _value = min;
}
// ملف الاختبار
import 'package:test/test.dart';
void main() {
group('Counter', () {
group('initial state', () {
test('starts at min value', () {
final c = Counter(min: 3, max: 10);
expect(c.value, equals(3));
});
test('defaults min=0, max=10', () {
final c = Counter();
expect(c.value, equals(0));
});
});
group('increment', () {
test('increases value by 1', () {
final c = Counter();
c.increment();
expect(c.value, equals(1));
});
test('does not exceed max', () {
final c = Counter(max: 2);
c.increment();
c.increment();
c.increment(); // ينبغي أن تُثبَّت القيمة عند الحد الأقصى
expect(c.value, equals(2));
});
});
group('reset', () {
test('restores value to min', () {
final c = Counter(min: 5, max: 20);
c.increment();
c.increment();
c.reset();
expect(c.value, equals(5));
});
});
});
}
التغطية المنهجية للحالات الحدية
الحالات الحدية هي المدخلات أو الظروف الواقعة عند حدود السلوك الصحيح والخاطئ. وهي مسؤولة عن نسبة غير متناسبة من أخطاء الإنتاج. إليك قائمة مرجعية عملية لكل أسلوب:
- المدخل الفارغ / المعدوم — ماذا يحدث حين يُحذف وسيط اختياري أو تكون مرجعية كائن null؟
- القيم الحدية — اختبر عند الحد بالضبط (مثل
max)، وما قبله بواحد (max - 1)، وما بعده بواحد (max + 1). - المجموعات الفارغة — قائمة فارغة، سلسلة نصية فارغة، خريطة ذات طول صفري.
- الأرقام السالبة — حين يُفترض أن المجال موجب فقط.
- مسارات الأخطاء — تأكد من رمي استثناءات أو كائنات
Errorبرسائل مفيدة عند انتهاك الشروط المسبقة.
اختبارات الحالات الحدية لمدقق كلمة المرور
// المدقق قيد الاختبار
class PasswordValidator {
static const int minLength = 8;
String? validate(String? password) {
if (password == null || password.isEmpty) {
return 'Password must not be empty';
}
if (password.length < minLength) {
return 'Password must be at least $minLength characters';
}
if (!password.contains(RegExp(r'[A-Z]'))) {
return 'Password must contain an uppercase letter';
}
return null; // صالح
}
}
// الاختبارات
void main() {
final validator = PasswordValidator();
group('PasswordValidator', () {
// حالات حدية: فارغة / معدومة
test('returns error for null password', () {
expect(validator.validate(null), isNotNull);
});
test('returns error for empty string', () {
expect(validator.validate(''), isNotNull);
});
// القيم الحدية
test('rejects password of exactly minLength - 1 chars', () {
final short = 'Abcdef1'; // 7 محارف
expect(validator.validate(short), isNotNull);
});
test('accepts password of exactly minLength chars', () {
final exact = 'Abcdef12'; // 8 محارف
expect(validator.validate(exact), isNull);
});
// مسار الخطأ
test('returns error when no uppercase letter is present', () {
expect(validator.validate('abcdefgh'), isNotNull);
});
// المسار السعيد
test('returns null for a fully valid password', () {
expect(validator.validate('Secure#99'), isNull);
});
});
}
اختبار تغييرات الحالة غير المتزامنة
تقوم كثير من الفئات الحقيقية بتحميل البيانات بصورة غير متزامنة. أحاط مثل هذه الاختبارات بـ async / await، واستخدم expectLater مع مطابقات التدفق حين تكشف الفئة عن Stream أو ValueNotifier.
خلاصة
الاختبارات المُهيكَلة جيداً تستخدم group() لمحاكاة شكل الكود المُختبَر. لكل فئة، تحقق من الحالة الأولية، وكل انتقال في الأساليب، والتسلسلات المنطقية، وجميع الحالات الحدية: المدخلات الفارغة، والقيم الحدية، والمجموعات الخالية، ومسارات الأخطاء. الاستخدام المنتظم لـ setUp() يُبقي الاختبارات مستقلة وموثوقة. هذا الانضباط يحوّل الاختبار من فكرة لاحقة إلى أداة تصميم تُحسّن كودك قبل إطلاقه.