Kodokon kodokon.com

الاختبارات: اختبارات Dart الوحدوية واختبارات الودجات

اختبر المنطق والودجات باستخدام نُسَخ زائفة (fakes) مكتوبة يدويًا، وساعة مُحاكاة، وردود الفعل الصحيحة تجاه pump.

11 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

هرم اختبارات Flutter: قاعدة عريضة من الاختبارات الوحدوية بلغة Dart الخالصة، وطبقة من اختبارات الودجات، وقليل من اختبارات التكامل. لا ينبغي لاختبار وحدوي أن يستورد أبدًا material.dart - فإن تطلّبه منطقك، فالمشكلة معمارية لا تقنية. ولعزل الكود عن تبعياته، فضِّل النُسَخ الزائفة (fakes) المكتوبة يدويًا على المحاكيات (mocks) المُولَّدة: فالنسخة الزائفة تنفيذ مبسّط لكنه وظيفي فعلًا للعقد (مستودع في الذاكرة، ساعة قابلة للتحكم)؛ وهي تصمد أمام عمليات إعادة الهيكلة وتُقرأ ككود عادي. أما المحاكيات، التي تتحقق من التفاعلات ("استُدعيت هذه الدالة مرتين")، فتربط الاختبار بتفاصيل التنفيذ - فاحتفظ بها للبروتوكولات التي يكون فيها التفاعل هو السلوك.

DART
import 'package:flutter/foundation.dart';

abstract interface class ScoreRepository {
  Future<int> fetchBest();
}

class ScoreController extends ChangeNotifier {
  ScoreController(this._repository);

  final ScoreRepository _repository;

  int? best;
  Object? error;

  Future<void> load() async {
    try {
      best = await _repository.fetchBest();
      error = null;
    } on Exception catch (e) {
      error = e;
    }
    notifyListeners();
  }
}
الكود قيد الاختبار: دون استيراد لـ material.dart.

يعيد setUp إنشاء الكائنات قبل كل اختبار: فلا حالة مشتركة، وبالتالي لا اختبار يعتمد على ترتيب التنفيذ - وذلك شرط موثوقيتها. وتنتج أدوات المطابقة القابلة للتركيب (isA<Exception>()، throwsA، isNull) رسائل فشل دقيقة، أكثر فائدة بكثير من قيمة منطقية. لاحظ الاختبار الثاني أدناه: فهو يتحقق من الفشل ثم من التعافي مباشرةً بعده. فمسارات الخطأ كود مثل أيّ كود آخر - وإن لم تُختبَر، فاعتبرها معطّلة.

DART
import 'package:flutter_test/flutter_test.dart';

class FakeScoreRepository implements ScoreRepository {
  int calls = 0;
  bool failNextCall = false;

  @override
  Future<int> fetchBest() async {
    calls++;
    if (failNextCall) {
      failNextCall = false;
      throw Exception('network unavailable');
    }
    return 42;
  }
}

void main() {
  late FakeScoreRepository repository;
  late ScoreController controller;

  setUp(() {
    repository = FakeScoreRepository();
    controller = ScoreController(repository);
  });

  test('exposes the best score', () async {
    await controller.load();

    expect(controller.best, 42);
    expect(controller.error, isNull);
    expect(repository.calls, 1);
  });

  test('captures an error then recovers', () async {
    repository.failNextCall = true;

    await controller.load();
    expect(controller.error, isA<Exception>());

    await controller.load();
    expect(controller.best, 42);
    expect(controller.error, isNull);
  });

  test('notifies its listeners', () async {
    var notifications = 0;
    controller.addListener(() => notifications++);

    await controller.load();

    expect(notifications, 1);
  });
}
نسخة زائفة من خمسة عشر سطرًا تحلّ محلّ مكتبة كاملة من المحاكيات.

لا يعرض اختبار الودجت أيّ شيء على الشاشة: فهو يعمل في ربط (binding) متخصّص (AutomatedTestWidgetsFlutterBinding) حيث يُحاكَى الزمن وحيث يُنتَج كل إطار عند الطلب. يركّب pumpWidget الشجرة؛ ويُقدّم pump() الساعة الزائفة ويُنتِج إطارًا واحدًا؛ ويقفز pump(const Duration(milliseconds: 150)) في الزمن - وهو مثالي لتجميد حالة حركة وسيطة وفحصها. وتستعلم أدوات Finder (find.text، find.byType، find.byKey) عن شجرة العناصر الحقيقية، لا عن لقطة شاشة. ولأنه لا شيء غير متزامن فعلًا - لا الساعة ولا الشبكة - فإن هذه الاختبارات حتمية (deterministic): فالفشل قابل لإعادة الإنتاج دائمًا.

DART
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class BadgeScreen extends StatefulWidget {
  const BadgeScreen({super.key});

  @override
  State<BadgeScreen> createState() => _BadgeScreenState();
}

class _BadgeScreenState extends State<BadgeScreen> {
  bool _visible = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          if (_visible) const Text('New'),
          IconButton(
            icon: const Icon(Icons.close),
            onPressed: () =>
                setState(() => _visible = false),
          ),
        ],
      ),
    );
  }
}

void main() {
  testWidgets('hides the badge after the tap',
      (tester) async {
    await tester.pumpWidget(
      const MaterialApp(home: BadgeScreen()),
    );
    expect(find.text('New'), findsOneWidget);

    await tester.tap(find.byIcon(Icons.close));
    await tester.pump();

    expect(find.text('New'), findsNothing);
  });
}
لا يُطلق tap أيّ إطار: فالـ pump الذي يليه ضروري.
BASH
flutter test
flutter test test/score_controller_test.dart
flutter test --coverage
flutter test --update-goldens
استهدف ملفًا، وقِس التغطية، وأعِد توليد الصور المرجعية (goldens).

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. لماذا يفشل pumpAndSettle أمام حركة بدأت بـ repeat()؟
    • يضخّ الإطارات ما دام هناك إطار مجدوَل: فالحركة اللانهائية لا تستقر أبدًا، ومن ثمّ تحدث مهلة زمنية
    • repeat() غير مدعوم في ربط الاختبار
    • pumpAndSettle لا يتعامل إلا مع الرسوم المتحركة الضمنية
  2. ما الفرق بين النسخة الزائفة (fake) والمحاكي (mock)؟
    • النسخة الزائفة تنفيذ مبسّط لكنه وظيفي؛ أما المحاكي فيخدم أساسًا للتحقق من التفاعلات
    • المحاكي مكتوب يدويًا، والنسخة الزائفة يولّدها أداة
    • لا فرق: فالمصطلحان مترادفان
    • لا تستطيع النسخة الزائفة تنفيذ واجهة برمجية في Dart
  3. في اختبار ودجت، ماذا يفعل await tester.pump() بالضبط؟
    • يُقدّم الساعة المُحاكاة ويُنتِج إطارًا جديدًا واحدًا
    • ينتظر انتهاء جميع الرسوم المتحركة الجارية
    • يعيد تشغيل الاختبار من البداية
    • ينتظر انتهاء طلبات الشبكة الحقيقية