فلٹر ایپس کی جانچ کیسے کریں: یونٹ، ویجیٹ، گولڈن، اور انٹیگریشن ٹیسٹنگ کی وضاحت

پہلی بار، مجھ سے سوال پوچھا گیا، "ٹیسٹنگ کا دائرہ کیا ہے؟” مجھے تکنیکی انٹرویو میں اچھے جواب نہیں ملے۔

تب تک، میں نے چند حقیقی فلٹر ایپس جاری کر دی تھیں۔ وہ کام کر رہے تھے اور صارف انہیں استعمال کر رہے تھے۔ لیکن میرے ٹیسٹ، اگر آپ انہیں کہہ سکتے ہیں، تو قیمتوں کا تعین کرنے کی فعالیت کے لیے صرف چند یونٹ ٹیسٹ تھے اور کچھ نہیں۔

کچھ مہینوں بعد میں نے کام کی تکمیل کے بہاؤ (ایک تبدیلی جو کہ فرق میں مکمل طور پر محفوظ لگ رہی تھی) کو ری فیکٹر کیا اور ایک چیز کو توڑ دیا جس کا میرے صارفین کو اصل خیال تھا۔ کسی کام کو مکمل کے بطور نشان زد کرنے سے اسے غلط فہرست سے ہٹا دیا گیا۔ کوئی کریش نہیں ہوا اور کوئی خرابیاں لاگ ان نہیں ہوئیں۔ صارفین نے خاموشی سے ایپ پر اعتماد کرنا چھوڑ دیا ہے۔ اور میں نے اس کے بارے میں ایک دوست سے بات کرکے معلوم کیا جو بیٹا ٹیسٹر تھا۔

یہ دراصل وہ بگ ہے جس کے لیے ٹیسٹنگ ہے۔ یہ حادثہ نہیں ہے کیونکہ حادثے کی اطلاع دی گئی تھی۔ ایک پرسکون رجعت جو صاف طور پر فراہم کرتی ہے اور اعتماد کو توڑتی ہے وہ ایسی چیز ہے جسے آپ صرف اس صورت میں پکڑ سکتے ہیں جب کچھ دیکھ رہا ہو۔

تب سے، ہم نے مزید ایپس جاری کی ہیں اور اب جان بوجھ کر ان کی جانچ کی ہے۔ سب کچھ نہیں، لیکن وہ جو اصل میں مجھے ناراض کرتے ہیں۔

یہ مضمون فلٹر کے ذریعے فراہم کردہ چار قسم کے ٹیسٹوں کا احاطہ کرتا ہے: یونٹ، ویجیٹ، سنہری، اور انضمام۔ ٹیسٹ ایک حقیقی دنیا کی خصوصیت کے ارد گرد بنایا گیا ہے اور چاروں سطحوں پر ٹیسٹ کیا جاتا ہے۔ چار منقطع ٹکڑوں کو پڑھنے نے مجھے یہ نہیں سکھایا کہ ان تہوں کو ایک ساتھ کیسے فٹ ہونا چاہئے، لہذا میں اسے اس طرح کرنے جا رہا ہوں۔ ان سب کو ایک ہی خصوصیات کے خلاف سجا ہوا دیکھ کر آخر کار اس پر کلک ہوا۔

انڈیکس

شرطیں

اس ٹیوٹوریل کے ساتھ آگے بڑھنے سے پہلے، آپ کو درج ذیل سے واقف ہونا چاہیے:

  • بنیادی ڈارٹ اور فلٹر نحو: بلڈنگ کلاسز، async/await اور سادہ ویجٹس۔ یہ شروع سے فلٹر ٹیوٹوریل نہیں ہے اور فرض کرتا ہے کہ آپ پہلے ہی اسکرینیں بنا سکتے ہیں۔ شاید انہوں نے ابھی تک اس کا صحیح تجربہ نہیں کیا ہے۔

  • فراہم کنندہ/تبدیل نوٹیفائر پیٹرن یا کچھ ایسا ہی (ریور پوڈ، بلاک، وغیرہ): TaskNotifier توسیع ChangeNotifierاور مثالوں سے اندازہ ہوتا ہے کہ آپ ریاستی انتظام کے اس انداز سے واقف ہیں، چاہے آپ کی ایپ کوئی مختلف ورژن استعمال کرے۔

آپ کو درج ذیل کو بھی انسٹال اور سیٹ اپ کرنا ہوگا:

  • فلٹر SDK (یہ تازہ ترین مستحکم ورژن ہے۔ مثالیں جدید ترین ٹیکنالوجی پر منحصر نہیں ہیں۔)

  • فلٹر/ڈارٹ کو سپورٹ کرنے والا ایڈیٹر (VS کوڈ یا اینڈرائیڈ اسٹوڈیو دونوں اچھی طرح کام کرتے ہیں)

  • ڈیوائس یا ایمولیٹر خاص طور پر انٹیگریشن ٹیسٹنگ سیکشن کے لیے۔ ایک iOS سمیلیٹر یا اینڈرائیڈ ایمولیٹر کافی ہوگا۔ جسمانی ہارڈ ویئر کی ضرورت نہیں ہے۔

  • اگلا ترقی انحصارجب بھی یہ سامنے آتا ہے اسے نمایاں کیا جاتا ہے، لیکن اس کا ہونا قابل قدر ہے۔

  dev_dependencies:
    flutter_test:
      sdk: flutter
    integration_test:
      sdk: flutter
    mocktail: ^1.0.4
    golden_toolkit: ^0.15.0

اگر میں دوڑ سکتا flutter test یہ ایک خالی پروجیکٹ میں صاف طور پر ختم ہوتا ہے، جانے کے لیے تیار ہے۔

ہمیں چار قسم کے ٹیسٹوں کی ضرورت کیوں ہے نہ کہ صرف "ٹیسٹ” کی؟

تمام ابتدائی فلٹر ٹیسٹنگ ٹیوٹوریلز جن کو میں نے "ٹیسٹنگ” کو ایک سرگرمی کے طور پر پڑھا ہے۔ یہ سچ نہیں ہے۔ چار قسمیں چار مختلف سوالوں کے جواب دیتی ہیں، اور جو چیز جانچ کو اتنا الجھا دیتی ہے وہ یہ ہے کہ آپ اسے کس طرح کرتے ہیں اس پر منحصر ہے کہ یہ حد سے زیادہ یا وقت کا ضیاع کیوں محسوس ہوتا ہے۔

یونٹ ٹیسٹنگ جواب: کیا منطق کا یہ خاص ٹکڑا دیے گئے ان پٹ کے لیے صحیح آؤٹ پٹ پیدا کرتا ہے؟ کوئی ویجٹ، رینڈرنگ یا نقلی آلات نہیں ہیں۔ آپ کو صرف ایک فنکشن یا کلاس، اور ایک دعویٰ کی ضرورت ہے۔ یہ ملی سیکنڈ میں چلتا ہے، اگر ضروری ہو تو ہزاروں۔

ویجیٹ ٹیسٹ جواب: کیا یہ ویجیٹ کچھ ریاستوں میں صحیح طریقے سے رینڈر اور کام کرتا ہے؟ یہ اصلی آلات یا اصلی پکسلز کے بغیر نقلی ماحول میں چلتا ہے۔ اور جب بھی آپ بچت کرتے ہیں تو چلانے کے لیے یہ کافی تیز ہے، لیکن پکڑنے کے لیے کافی حقیقت پسندانہ ہے "اگر درخواست ناکام ہوجاتی ہے، تو آپ کو دوبارہ کوشش کرنے کا بٹن نظر نہیں آئے گا۔”

سنہری ٹیسٹ جواب: کیا یہ ویجیٹ اب بھی دستیاب ہے؟ دیکھو کیا یہ وہی ہے؟ رینڈر کردہ ویجیٹ پکسل بہ پکسل کا ایک ذخیرہ شدہ حوالہ تصویر کے ساتھ موازنہ کرتا ہے۔ یہ چاروں میں سے صرف ایک ہے جو "پیڈنگ اب غلط ہے” یا "ٹیکسٹ اوور فلو” کو پکڑ سکتا ہے۔ یہ چیزیں انسانوں کے لیے بصری طور پر ظاہر ہیں لیکن انسانوں کے لیے پوشیدہ ہیں۔ find.text() تضاد

انضمام کی جانچ جواب: کیا حقیقی ایپس جو اصلی یا نقلی آلات پر مرتب اور چلتی ہیں دراصل آخر سے آخر تک کام کرتی ہیں؟ یہ سست ہے، نسبتاً مہنگا ہے، اور ان چار میں سے صرف ایک ہے جو صرف کراس لیئر تعاملات میں موجود کیڑے پکڑ سکتا ہے، جیسے کہ کوئی اسٹور غلط قسم کو نوٹیفائر کو واپس کرتا ہے جو بہرحال درست طریقے سے پیش کرتا ہے۔

چار میں سے کوئی بھی دوسرے کی جگہ نہیں لیتا۔ قیمتوں کے کیڑے یونٹ ٹیسٹ میں ہیں۔ گمشدہ خرابی کی شرائط ویجیٹ ٹیسٹ سے تعلق رکھتی ہیں۔ منتقل شدہ ترتیب گولڈن ٹیسٹ کے تحت آتا ہے۔ ٹوٹے ہوئے اینڈ ٹو اینڈ فلوز انضمام ٹیسٹ سے تعلق رکھتے ہیں۔ چار میں سے صرف ایک کو استعمال کرنے کا مطلب ہے کہ تین قسم کے کیٹگریز کا پتہ نہ چل سکا۔

خصوصیات کی جانچ کی جا رہی ہے۔

اس کنکریٹ کو برقرار رکھنے کے لیے، تمام سیکشنز ایک ہی فعالیت کے ارد گرد بنائے گئے ہیں: ایک ٹاسک لسٹ جہاں صارف کاموں کو مکمل کے طور پر نشان زد کر سکتے ہیں، جس تعداد کے ساتھ وہ ایپ بار میں ظاہر ہوتے ہیں۔

// lib/task.dart
class Task {
  const Task({required this.id, required this.title, this.isDone = false});

  final String id;
  final String title;
  final bool isDone;

  Task copyWith({bool? isDone}) =>
      Task(id: id, title: title, isDone: isDone ?? this.isDone);
}
// lib/task_repository.dart
abstract class TaskRepository {
  Future> fetchTasks();
  Future setTaskDone(String id, bool isDone);
}
// lib/task_logic.dart

/// The bug I actually shipped: this used to filter on the wrong
/// field when a task list contained tasks from more than one list,
/// silently completing a task in the wrong place. A single unit
/// test on this function would have caught it before it shipped.
int countCompleted(List tasks) =>
    tasks.where(

List markDone(List tasks, String id) => tasks
    .map(
    .toList();
// lib/task_notifier.dart
class TaskNotifier extends ChangeNotifier {
  TaskNotifier(this._repository);
  final TaskRepository _repository;

  List _tasks = [];
  bool isLoading = false;
  String? error;

  List get tasks => _tasks;
  int get completedCount => countCompleted(_tasks);

  Future load() async {
    isLoading = true;
    error = null;
    notifyListeners();

    try {
      _tasks = await _repository.fetchTasks();
    } catch (_) {
      error="Failed to load tasks. Please try again.";
    }

    isLoading = false;
    notifyListeners();
  }

  Future complete(String id) async {
    final previous = _tasks;
    _tasks = markDone(_tasks, id); // optimistic update
    notifyListeners();

    try {
      await _repository.setTaskDone(id, true);
    } catch (_) {
      _tasks = previous; // roll back on failure
      notifyListeners();
    }
  }
}
// lib/task_screen.dart
class TaskScreen extends StatefulWidget {
  const TaskScreen({super.key, required this.notifier});
  final TaskNotifier notifier;

  @override
  State createState() => _TaskScreenState();
}

class _TaskScreenState extends State {
  @override
  void initState() {
    super.initState();
    widget.notifier.load();
  }

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: widget.notifier,
      builder: (context, _) {
        final notifier = widget.notifier;

        return Scaffold(
          appBar: AppBar(title: Text('Tasks (${notifier.completedCount} done)')),
          body: notifier.isLoading
              ? const Center(child: CircularProgressIndicator())
              : notifier.error != null
                  ? Center(
                      child: Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Text(notifier.error!),
                          const SizedBox(height: 8),
                          ElevatedButton(
                            onPressed: notifier.load,
                            child: const Text('Retry'),
                          ),
                        ],
                      ),
                    )
                  : ListView(
                      children: notifier.tasks
                          .map((task) => CheckboxListTile(
                                key: ValueKey(task.id),
                                title: Text(task.title),
                                value: task.isDone,
                                onChanged: task.isDone
                                    ? null
                                    : (_) => notifier.complete(task.id),
                              ))
                          .toList(),
                    ),
        );
      },
    );
  }
}

یہ مکمل فعالیت ہے۔ اب ہم اسے چار طریقوں سے جانچتے ہیں:

یونٹ ٹیسٹنگ: الگ تھلگ کاروباری منطق

countCompleted اور markDone یہ ایک باقاعدہ ڈارٹ فنکشن ہے جس میں کوئی فلٹر انحصار نہیں ہے۔ نہیں BuildContextکوئی بھی چیز جس کی آپ کو ویجیٹ یا ٹیسٹ رگ کی ضرورت ہو۔ یہ جان بوجھ کر ہے۔ اس اہم منطق کے لیے رینڈرنگ انجن کی تصدیق کی ضرورت نہیں ہے۔

// test/task_logic_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:my_app/task.dart';
import 'package:my_app/task_logic.dart';

void main() {
  group('countCompleted', () {
    test('returns 0 for an empty list', () {
      expect(countCompleted([]), 0);
    });

    test('counts only tasks marked done', () {
      final tasks = [
        const Task(id: '1', title: 'A', isDone: true),
        const Task(id: '2', title: 'B', isDone: false),
        const Task(id: '3', title: 'C', isDone: true),
      ];

      expect(countCompleted(tasks), 2);
    });
  });

  group('markDone', () {
    test('marks only the task with the matching id', () {
      final tasks = [
        const Task(id: '1', title: 'A'),
        const Task(id: '2', title: 'B'),
      ];

      final result = markDone(tasks, '2');

      // The critical assertion — this is the exact bug I shipped.
      // A naive implementation that filters on the wrong field
      // would mark task '1' done instead, or both, and this
      // test would fail immediately instead of surfacing in
      // a user's bug report three weeks later.
      expect(result.firstWhere(
      expect(result.firstWhere(
    });

    test('returns an unchanged list if the id does not exist', () {
      final tasks = [const Task(id: '1', title: 'A')];
      final result = markDone(tasks, 'nonexistent');

      expect(result.first.isDone, false);
    });
  });
}

اسے استعمال کرتے ہوئے چلائیں:

flutter test test/task_logic_test.dart

ہر ٹیسٹ چند ملی سیکنڈ میں چلتا ہے۔ ان ٹیسٹوں کو لکھنے کو چھوڑنے کی کوئی وجہ نہیں ہے۔ یہ ایک پرت ہے جہاں ناقص مفروضوں کو خود بخود پیداوار میں دھکیل دیا جاتا ہے کیونکہ لاگت صفر کے قریب ہوتی ہے اور اگر منطق بالکل غلط ہو تو کچھ بھی مختلف نہیں ہوتا۔ چیک باکس اب بھی ٹوگل ہوگا، لیکن یہ صرف غلط کارروائی کو ٹوگل کرے گا۔

ایک عادت ایسی ہے جو بچپن سے ہی نشوونما پاتی ہے۔ group تقریبا ایک جیسے ٹیسٹوں کو کاپی اور پیسٹ کرنے کے بجائے، پیرامیٹرائزڈ اسٹائل لوپ استعمال کریں۔ میں ایک ہی فنکشن کے پانچ انتہائی کیسز کے لیے تقریباً ایک جیسے ٹیسٹ فنکشنز لکھتا تھا، اور لامحالہ ان پانچوں میں سے ایک ری فیکٹرنگ کے بعد دوسروں کے ساتھ ہم آہنگی سے باہر ہو جائے گا۔

group('markDone with various ids', () {
  final cases = {
    '1': true,   // exists, should be marked done
    '2': false,  // exists, different id, should stay unchanged
    'x': false,  // does not exist, should be a no-op
  };

  for (final entry in cases.entries) {
    test('id ${entry.key} resolves to isDone=${entry.value}', () {
      final tasks = [
        const Task(id: '1', title: 'A'),
        const Task(id: '2', title: 'B'),
      ];
      final result = markDone(tasks, '1');
      final target = result.where(

      if (target.isEmpty) {
        // The 'x' case — id doesn't exist, list should be unaffected
        expect(result.length, tasks.length);
      } else {
        expect(target.first.isDone, entry.value);
      }
    });
  }
});

یہ دو یا تین کیسز کے لیے سختی سے ضروری نہیں ہے، لیکن جس وقت کسی فنکشن کی جانچ کے قابل پانچ یا چھ شاخیں ہوتی ہیں، ایک لوپ ارادے کو پڑھنے کے قابل رکھتا ہے اور چھٹے کیس کو کاپی اور پیسٹ کیے گئے ٹیسٹ فنکشن کے بجائے ایک لائن کی تبدیلی کے ساتھ جوڑتا ہے جسے کوئی صحیح طریقے سے اپ ڈیٹ کرنا بھول گیا ہے۔

غیر مطابقت پذیر منطق اور استثناء کی جانچ

حقیقی ایپس میں زیادہ تر دلچسپ منطق خالصتاً مطابقت پذیر نہیں ہوتی۔ کیونکہ یہ غیر مطابقت پذیر ہے، یہ ناکام ہو سکتا ہے۔ flutter_testایس test() ہینڈل Future– یہ باڈی کو بطور ڈیفالٹ واپس کرتا ہے، جو لوگوں کی توقع سے زیادہ آسان بناتا ہے، لیکن اس کے خودکار ہونے سے پہلے میں نے بار بار دو غلطیاں کیں۔

test('setTaskDone throws for an unknown task id', () async {
  final repository = FakeTaskRepository();

  // expect() with throwsA works on synchronous throws.
  // For a Future that completes with an error, you need
  // expectLater with throwsA, or the async matcher form below.
  await expectLater(
    () => repository.setTaskDone('nonexistent', true),
    throwsA(isA()),
  );
});

test('fetchTasks returns an empty list, not null, when there is nothing to fetch', () async {
  final repository = FakeTaskRepository(seed: []);

  final result = await repository.fetchTasks();

  // This looks trivial, but I've genuinely shipped a null check
  // in a widget that assumed an empty repository always threw
  // instead of returning []. One line here would have caught it.
  expect(result, isEmpty);
  expect(result, isNotNull);
});

میری سب سے عام ابتدائی غلطی: لکھنا۔ expect(() => someAsyncFunction(), throwsA(...)) بغیر await اس کے سامنے. چونکہ ٹیسٹ کے تحت فنکشن غیر مطابقت پذیر ہے، استثناء ہے۔ Future اگر ہم وقت ساز مسئلہ ابھی تک حل نہیں ہوا ہے۔ expect() دوڑو۔ چونکہ حقیقت میں کوئی بھی چیز ناکامی کے ہونے کا انتظار نہیں کرتی ہے، اس لیے کوڈ ٹوٹ جانے پر بھی ٹیسٹ خود بخود پاس ہو جاتا ہے۔ expectLater کے ساتھ مل کر await یہ وہ ورژن ہے جو دراصل ناکامی کا راستہ چلاتا ہے۔

TaskScreen چاہے یہ لوڈ ہو رہا ہو، غلطیاں دکھا رہا ہو، یا ڈیٹا ڈسپلے کر رہا ہو، اسے صحیح طریقے سے رینڈر کرنے کی ضرورت ہے۔ اور مجھے ایک جعلی کی ضرورت ہے۔ TaskRepository ایک حقیقی نیٹ ورک کال کیے بغیر ایسا کرنے کے لیے۔ mocktail یہ ڈارٹ میں موجودہ معیار ہے کیونکہ اسے پچھلے فرضی طریقوں کی طرح کوڈ جنریشن کی ضرورت نہیں ہے۔

dev_dependencies:
  mocktail: ^1.0.4
// test/task_screen_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/task.dart';
import 'package:my_app/task_notifier.dart';
import 'package:my_app/task_repository.dart';
import 'package:my_app/task_screen.dart';

class MockTaskRepository extends Mock implements TaskRepository {}

void main() {
  late MockTaskRepository repository;

  setUp(() {
    repository = MockTaskRepository();
  });

  testWidgets('shows a loading indicator while fetching', (tester) async {
    // A Completer that never resolves keeps the widget in the
    // loading state for the duration of this specific test.
    repository.fetchTasks; // registered below via when()
    when(() => repository.fetchTasks())
        .thenAnswer((_) => Completer>().future);

    await tester.pumpWidget(MaterialApp(
      home: TaskScreen(notifier: TaskNotifier(repository)),
    ));

    // pump() advances exactly one frame — enough to see the
    // loading state, without waiting for anything to resolve.
    await tester.pump();

    expect(find.byType(CircularProgressIndicator), findsOneWidget);
  });

  testWidgets('shows tasks once loaded', (tester) async {
    when(() => repository.fetchTasks()).thenAnswer(
      (_) async => [
        const Task(id: '1', title: 'Buy milk'),
        const Task(id: '2', title: 'Walk the dog', isDone: true),
      ],
    );

    await tester.pumpWidget(MaterialApp(
      home: TaskScreen(notifier: TaskNotifier(repository)),
    ));

    // pumpAndSettle waits for all pending frames and microtasks —
    // the right call once you want to assert on the final,
    // settled state rather than a specific frame along the way.
    await tester.pumpAndSettle();

    expect(find.text('Buy milk'), findsOneWidget);
    expect(find.text('Tasks (1 done)'), findsOneWidget);
  });

  testWidgets('shows an error state with a working retry button', (tester) async {
    when(() => repository.fetchTasks()).thenThrow(Exception('network error'));

    await tester.pumpWidget(MaterialApp(
      home: TaskScreen(notifier: TaskNotifier(repository)),
    ));
    await tester.pumpAndSettle();

    expect(find.text('Failed to load tasks. Please try again.'), findsOneWidget);

    // Now make the retry succeed, and confirm tapping Retry
    // actually recovers — not just that the button exists.
    when(() => repository.fetchTasks())
        .thenAnswer((_) async => [const Task(id: '1', title: 'Buy milk')]);

    await tester.tap(find.text('Retry'));
    await tester.pumpAndSettle();

    expect(find.text('Buy milk'), findsOneWidget);
    expect(find.text('Failed to load tasks. Please try again.'), findsNothing);
  });

  testWidgets('completing a task updates the done count', (tester) async {
    when(() => repository.fetchTasks()).thenAnswer(
      (_) async => [const Task(id: '1', title: 'Buy milk')],
    );
    when(() => repository.setTaskDone('1', true)).thenAnswer((_) async {});

    await tester.pumpWidget(MaterialApp(
      home: TaskScreen(notifier: TaskNotifier(repository)),
    ));
    await tester.pumpAndSettle();

    expect(find.text('Tasks (0 done)'), findsOneWidget);

    await tester.tap(find.byType(CheckboxListTile));
    await tester.pumpAndSettle();

    expect(find.text('Tasks (1 done)'), findsOneWidget);
  });
}

دوبارہ کوشش کرنے کا ٹیسٹ ایک قابل ذکر امتحان ہے۔ یہ "دوبارہ کوشش کرنے والا بٹن ظاہر ہوتا ہے” پر رکنے کے لیے پرکشش ہے، لیکن اس سے صرف یہ ثابت ہوتا ہے کہ بٹن موجود ہے اور اسے ٹیپ کرنے سے کچھ نہیں ہوتا۔ بازیافت شدہ حالت کا سراغ لگانا اور اس پر زور دینا ایک خراب کال بیک کے ساتھ دوبارہ کوشش کرنے والے بٹن کو جوڑنے سے گریز کرنا ہے، جو کہ عملی طور پر کرنا ایک آسان غلطی ہے۔

میں نے ان سب کو ایک سے زیادہ بار بنایا ہے۔

1. استعمال کریں۔ pump() جب آپ کا مطلب تھا pumpAndSettle()یا اس کے برعکس۔

pump() بالکل ایک فریم کو آگے بڑھاتا ہے۔ pumpAndSettle() فریم کو اس وقت تک پمپ کرتے رہیں جب تک کہ آپ اسے دوبارہ بنانے کا منصوبہ نہ بنائیں۔ غیر مطابقت پذیر آپریشن مکمل ہونے کے بعد آپ یہی چاہتے ہیں، لیکن اگر ویجیٹ ٹری میں کوئی چیز متحرک ہوتی رہتی ہے، تو یہ غیر معینہ مدت تک لٹک جائے گی (آخر کار وقت ختم ہو جائے گا)۔ CircularProgressIndicator.

میں یہ سمجھنے سے پہلے ٹیسٹ ٹائم آؤٹ کے بارے میں الجھن میں پڑ گیا ہوں کہ لوڈنگ اسپنر ہی، جس میں لامحدود اینیمیشن ہے، اس کی وجہ کیا ہے۔ pumpAndSettle میں نے پہلے کبھی منجمد فریم نہیں دیکھا۔

آپ کے مخصوص کیس میں حل کال کرنا ہے۔ pump() اوقات کی ایک مقررہ تعداد، یا pump(duration) اس کے بجائے، ایک واضح مدت استعمال کریں۔ pumpAndSettle()جب بھی ٹیسٹ کے تحت ویجیٹ میں کوئی ایسی چیز ہوتی ہے جس کی حرکت پذیری جائز طور پر بند نہیں ہوتی ہے۔

// This will time out if the tree contains a CircularProgressIndicator,
// which animates forever and never "settles."
await tester.pumpAndSettle();

// This advances a fixed number of frames instead — the right
// choice when you specifically want to catch the loading state
// mid-flight rather than wait for it to resolve.
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));

2. متن کے ذریعہ ویجٹ تلاش کریں۔ Key یہ زیادہ مستحکم ہوگا۔

find.text('Buy milk') یہ اس لمحے کو توڑ دیتا ہے جب پروڈکٹ کی کاپی تبدیل ہوتی ہے، یا اگر دو کام مستقبل کے ٹیسٹوں میں ایک عنوان کا اشتراک کرتے ہیں۔ اب ہر وہ چیز درج کریں جو آپ کو ٹیسٹ میں قابل اعتماد طریقے سے تلاش کرنے کی ضرورت ہے، اسی طرح۔ CheckboxListTile مندرجہ بالا چابیاں ہیں: ValueKey(task.id): find.byKey(const ValueKey('1')) مجھے اس سے کوئی فرق نہیں پڑتا کہ نوکری کا عنوان کیا ہے۔

3. اسے بھول جانا MaterialApp اس کے چھونے والے تمام ویجیٹ ٹیسٹوں کو لپیٹ دیتا ہے۔ Theme.of(context) یا Navigator.

دور اندیشی pumpWidget(TaskScreen(...)) بغیر MaterialApp اجداد گمشدگی کے بارے میں ایک مبہم غلطی پھینکتا ہے۔ Directionality یا Navigator پہلی بار جب کوئی ویجیٹ کسی ایسے آپریشن کی کوشش کرتا ہے جو دونوں پر منحصر ہوتا ہے۔ یہ وہ غلطی کا پیغام ہے جو آپ کو پہلی چند بار مارنے پر ملتا ہے، جو ظاہر ہے "مٹیریل ایپ میں لپیٹ” کی طرف اشارہ نہیں کرتا ہے۔

ٹیکسٹ ان پٹ اور اسکرولنگ کی جانچ کریں۔

یہ دو تعاملات – کسی فیلڈ میں ٹائپ کرنا اور آف اسکرین مواد کو ظاہر کرنے کے لیے اسکرول کرنا – اکثر ایسا ہوتا ہے کہ اپنے طور پر اس کی مثال دینے کے قابل ہو۔

testWidgets('typing a name and submitting calls the repository', (tester) async {
  final repository = MockTaskRepository();
  when(() => repository.fetchTasks()).thenAnswer((_) async => []);
  when(() => repository.addTask(any())).thenAnswer((_) async {});

  await tester.pumpWidget(MaterialApp(
    home: TaskScreen(notifier: TaskNotifier(repository)),
  ));
  await tester.pumpAndSettle();

  // enterText simulates typing directly — no need to simulate
  // individual keystrokes for the overwhelming majority of tests.
  await tester.enterText(find.byKey(const Key('new_task_field')), 'Buy milk');
  await tester.tap(find.byKey(const Key('add_task_button')));
  await tester.pumpAndSettle();

  verify(() => repository.addTask('Buy milk')).called(1);
});

testWidgets('scrolling reveals a task below the fold', (tester) async {
  final repository = MockTaskRepository();
  when(() => repository.fetchTasks()).thenAnswer(
    (_) async => List.generate(
      30,
      (i) => Task(id: '$i', title: 'Task $i'),
    ),
  );

  await tester.pumpWidget(MaterialApp(
    home: TaskScreen(notifier: TaskNotifier(repository)),
  ));
  await tester.pumpAndSettle();

  // Task 25 is off-screen on first render in a 30-item list.
  expect(find.text('Task 25'), findsNothing);

  // scrollUntilVisible repeatedly scrolls a fixed amount and
  // checks after each attempt — the right tool when you don't
  // know exactly how far to scroll to reach a specific item.
  await tester.scrollUntilVisible(
    find.text('Task 25'),
    500.0,
    scrollable: find.byType(Scrollable),
  );

  expect(find.text('Task 25'), findsOneWidget);
});

گولڈن ٹیسٹ: بصری رجعت کو پکڑنا

اب تک تمام ٹیسٹ ہو چکے ہیں۔ کارروائی: درست متن ظاہر ہوتا ہے یا درست شمار کو اپ ڈیٹ کیا جاتا ہے۔ ان میں سے کوئی بھی ایسی تبدیلیوں کو نہیں پکڑے گا جس کی وجہ سے چھوٹی اسکرینوں پر چیک باکس کی فہرست کنٹینر کو اوور فلو کرتی ہے، یا پیڈنگ ایڈجسٹمنٹ جو دوبارہ کوشش کرنے کے بٹن کو اسکرین سے دور کرتی ہے۔ یہی وجہ ہے کہ سنہری امتحان موجود ہے۔

گولڈن ٹیسٹ ویجیٹ کو پیش کرتا ہے اور اس کا موازنہ ایک ذخیرہ شدہ حوالہ تصویر سے پکسل بہ پکسل کرتا ہے۔

// test/task_screen_golden_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:my_app/task.dart';
import 'package:my_app/task_notifier.dart';
import 'package:my_app/task_repository.dart';
import 'package:my_app/task_screen.dart';

class MockTaskRepository extends Mock implements TaskRepository {}

void main() {
  testWidgets('task screen with data matches the golden file', (tester) async {
    final repository = MockTaskRepository();
    when(() => repository.fetchTasks()).thenAnswer(
      (_) async => [
        const Task(id: '1', title: 'Buy milk'),
        const Task(id: '2', title: 'Walk the dog', isDone: true),
      ],
    );

    await tester.pumpWidget(MaterialApp(
      home: TaskScreen(notifier: TaskNotifier(repository)),
    ));
    await tester.pumpAndSettle();

    // On first run, this generates the reference image.
    // On every run after, it fails if a single pixel differs.
    await expectLater(
      find.byType(TaskScreen),
      matchesGoldenFile('goldens/task_screen_with_data.png'),
    );
  });

  testWidgets('task screen error state matches the golden file', (tester) async {
    final repository = MockTaskRepository();
    when(() => repository.fetchTasks()).thenThrow(Exception('error'));

    await tester.pumpWidget(MaterialApp(
      home: TaskScreen(notifier: TaskNotifier(repository)),
    ));
    await tester.pumpAndSettle();

    await expectLater(
      find.byType(TaskScreen),
      matchesGoldenFile('goldens/task_screen_error.png'),
    );
  });
}

استعمال کرتے ہوئے ایک ابتدائی حوالہ تصویر بنائیں:

flutter test --update-goldens test/task_screen_golden_test.dart

عزم پیدا کیا .png ٹیسٹ کے ساتھ فائل کریں۔ تب سے، flutter test دوبارہ تخلیق کرنے کے بجائے موازنہ کریں۔ اگر مستقبل میں تبدیلی پکسلز کو تبدیل کرنے کا سبب بنتی ہے، تو ٹیسٹ ناکام ہو جائے گا اور فرق ظاہر کرے گا، بجائے اس کے کہ آپ کی ٹیم تین اسپرنٹ کے بعد اصلی ڈیوائس پر اسکرین کو دیکھ کر تھوڑا سا عجیب لگتی ہے۔

اس پر بہت زیادہ انحصار کرنے سے پہلے دو انتباہات سے آگاہ ہونا ضروری ہے: پہلا، فونٹس اور رینڈرنگ آپ کے کمپیوٹر اور CI ایگزیکیوٹر کے درمیان بالکل مختلف ہو سکتے ہیں، جس کی وجہ سے غلط غلطیاں ہو سکتی ہیں جن کا آپ کے کوڈ سے کوئی تعلق نہیں ہے۔ گولڈن کو مستقل ڈوکر امیج کے اندر چلائیں یا اس طرح کا پیکیج استعمال کریں: golden_toolkit (معیاری فونٹ لوڈنگ) اس مسئلے میں سے زیادہ تر حل کرتا ہے۔

دوسرا، گولڈن ان اسکرینوں پر برقرار رکھنا مہنگا ہے جو فعال نشوونما کے دوران اکثر تبدیل ہوتی ہیں۔ ہر بار جب آپ لے آؤٹ کو ایڈجسٹ کرتے ہیں تو گولڈن کو دوبارہ بنانا مقصد کو ناکام بنا دیتا ہے، اس لیے اسے باقی چیزوں کے بجائے مستحکم، زیادہ مرئی اسکرینوں کے لیے محفوظ کریں۔

ملٹی ڈیوائس اور ڈارک موڈ گولڈن

ایک سنہری تصویر صرف یہ ثابت کرتی ہے کہ اسکرین ایک تھیم اور ایک اسکرین سائز میں درست نظر آتی ہے۔ میں نے اصل میں جو بگ پایا وہ یہ تھا کہ ورکنگ ٹائٹل، جسے معیاری فون کی چوڑائی پر صاف ستھرا کرپ کیا گیا تھا، چھوٹی اسکرین ڈیوائسز پر 9 پکسلز تک پھیل گیا، اور کسی نے اس وقت تک توجہ نہیں دی جب تک کہ پرانے، تنگ فون کا استعمال کرنے والے کسی کی طرف سے سپورٹ ٹکٹ نہ آئے۔

golden_toolkitایس multiScreenGolden ایک ہی ٹیسٹ میں ایک ہی ویجیٹ کو متعدد ڈیوائس سائزز میں رینڈر کریں۔ یہ وہ ورژن ہے جو میں ان تمام اسکرینوں پر استعمال کرتا ہوں جن کی میں سنہری جانچ کر رہا ہوں۔

dev_dependencies:
  golden_toolkit: ^0.15.0
testGoldens('task screen across device sizes', (tester) async {
  final repository = MockTaskRepository();
  when(() => repository.fetchTasks()).thenAnswer(
    (_) async => [const Task(id: '1', title: 'Buy milk, eggs, and bread')],
  );

  final builder = DeviceBuilder()
    ..overrideDevicesForAllScenarios(devices: [
      Device.phone,       // narrow — this is the one that caught the overflow
      Device.iphone11,
      Device.tabletLandscape,
    ])
    ..addScenario(
      widget: MaterialApp(home: TaskScreen(notifier: TaskNotifier(repository))),
      name: 'with data',
    );

  await tester.pumpDeviceBuilder(builder);
  await screenMatchesGolden(tester, 'task_screen_multi_device');
});

اگر آپ کی ایپ اس کی حمایت کرتی ہے تو ڈارک موڈ اسی پروسیسنگ کے قابل ہے۔ ہارڈ کوڈڈ ٹیکسٹ کلر جو کہ گہرے پس منظر میں پوشیدہ ہوتے ہیں کیڑے کی ایک حقیقی شرمناک کلاس ہیں، اور اگر آپ کے تمام سنہری ٹیسٹ صرف لائٹ موڈ میں پیش کیے جائیں تو وہ مکمل طور پر پوشیدہ ہیں۔

testGoldens('task screen in dark mode', (tester) async {
  final repository = MockTaskRepository();
  when(() => repository.fetchTasks()).thenAnswer(
    (_) async => [const Task(id: '1', title: 'Buy milk')],
  );

  await tester.pumpWidgetBuilder(
    TaskScreen(notifier: TaskNotifier(repository)),
    wrapper: materialAppWrapper(theme: ThemeData.dark()),
  );
  await tester.pumpAndSettle();

  await screenMatchesGolden(tester, 'task_screen_dark_mode');
});

اپنے گولڈن کو دیکھ بھال کا بوجھ بننے سے روکیں۔

میں نے جو ناکامی کا موڈ دیکھا ہے وہ وہ ہے جہاں ایک سے زیادہ ٹیمیں ایک مہینے کے لیے سنہری رنگ کا اضافہ کرتی ہیں اور پھر ایک جائز ڈیزائن کی تبدیلی 15 اسکرینوں پر استعمال ہونے والے مشترکہ جزو کو متاثر کرتی ہے۔ پھر تمام 15 گولڈن ٹیسٹ بیک وقت ناکام ہو جاتے ہیں اور ٹیم رنز بناتی ہے۔ --update-goldens اصل میں ہر فرق کی جانچ کیے بغیر۔ آخر میں، انفرادی طور پر 15 تصویری فرقوں سے گزرنا ایسا محسوس نہیں کرتا کہ یہ آپ کے وقت کے قابل ہے۔

اس لمحے سے، سنہری امتحان آپ کی حفاظت کرنا چھوڑ دیتا ہے۔ کیونکہ اس کے بعد سے، ٹیم کا اضطراب "کھیلنا اور آگے بڑھنا” ہے بجائے اس کے کہ "کیا بدلا ہے اور دیکھیں کہ کیا یہ جان بوجھ کر ہے۔”

دو چیزیں ایسا ہونے سے روکتی ہیں۔ سب سے پہلے، اپنے سنہری سیٹ کو چھوٹا رکھیں اور اسے خاص طور پر منتخب کریں۔ میں نے اوپر اس کا تذکرہ کیا ہے، لیکن یہ دہرانے کے لیے کافی ضروری ہے۔ 5 یا 6 ہائی ویلیو اسکرینز کا انتخاب کریں، 50 نہیں۔

دوسرا، کسی باکس کو غیر چیک کرنے کے بجائے، اصل میں فرق کو کھولنے کے لیے سنہری ناکامیوں کے سلسلے کو ایک سگنل کے طور پر سمجھیں۔ گولڈن ٹیسٹنگ کے لیے زیادہ تر CI سیٹ اپ خاص طور پر مختلف تصاویر کو تعمیراتی نمونوں کے طور پر اپ لوڈ کرنے کی اجازت دیتے ہیں، لہذا جائزہ لینے والے ان تصاویر کو مقامی طور پر برانچ کو کھینچے بغیر پل کی درخواست میں دیکھ سکتے ہیں۔

انٹیگریشن ٹیسٹنگ: پوری ایپ، اینڈ ٹو اینڈ

یونٹ اور ویجیٹ ٹیسٹ نقلی ڈارٹ ماحول میں چلائے جاتے ہیں۔ کوئی حقیقی رینڈرنگ انجن یا پلیٹ فارم چینل نہیں ہے، ایک جعلی ذخیرہ نیٹ ورک پر قبضہ کر لیتا ہے۔ یہی چیز انہیں تیز تر بناتی ہے، اور یہی چیز انہیں مضحکہ خیز بناتی ہے۔ یعنی، چاہے ایک حقیقی ایپ مرتب کی گئی ہو اور ایک حقیقی ڈیوائس یا ایمولیٹر پر چل رہی ہو، اصل میں اس وقت کام کرتی ہے جب تمام پرتیں ایک ساتھ جڑی ہوں۔

dev_dependencies:
  integration_test:
    sdk: flutter
// integration_test/complete_task_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  testWidgets('user can load tasks and complete one, end to end', (tester) async {
    // This runs your actual main() — the real app, the real
    // repository implementation, whatever backend it's wired to
    // in this build (typically a staging environment for CI).
    app.main();
    await tester.pumpAndSettle();

    expect(find.text('Buy milk'), findsOneWidget);
    expect(find.textContaining('0 done'), findsOneWidget);

    await tester.tap(find.byType(CheckboxListTile).first);
    await tester.pumpAndSettle();

    expect(find.textContaining('1 done'), findsOneWidget);
  });
}

اسے اصلی ڈیوائس یا ایمولیٹر کے خلاف چلائیں۔

flutter test integration_test/complete_task_test.dart

یہ سست ہے (ملی سیکنڈ کے بجائے سیکنڈ) کیونکہ یہ مصنوعی ویجیٹ ٹری کے بجائے اصل ایپ کو مرتب اور چلاتا ہے۔ ان اخراجات کی وجہ سے، انضمام کے ٹیسٹ میں ہر اسکرین کے بجائے بہت کم تعداد میں بہاؤ کا احاطہ کرنا چاہیے جو درحقیقت نقصان کا باعث بن سکتے ہیں، جیسے کہ خریداری مکمل کرنا یا لاگ ان کرنا (یعنی بنیادی کام جو ایپ کسی کو کرنے کی اجازت دینے کے لیے موجود ہے) ہر اسکرین کے بجائے۔ میں ان میں سے 5 یا 6 کو حقیقی پروجیکٹس میں چلاتا ہوں اور وہ اس بہاؤ کا احاطہ کرتے ہیں جو میں اپنے صارفین سے پہلے جاننا چاہتا ہوں۔

عدم استحکام، دوبارہ کوششیں، اور حقیقی آلات

انٹیگریشن ٹیسٹ اس طرح ناکام ہو جاتے ہیں جس طرح دیگر تین اقسام میں سے اکثر ناکام ہو جاتے ہیں: یعنی، یہ کیڑے سے غیر متعلق وجوہات کی بنا پر وقفے وقفے سے ناکام ہو جاتا ہے۔ ایک CI ایمولیٹر معمول سے زیادہ آہستہ چل رہا ہے، سٹیجنگ ماحول پر نیٹ ورک کال جس میں ٹیسٹ کی توقع سے آدھا سیکنڈ زیادہ وقت لگتا ہے، اگلا ٹاسک چلنے پر اینیمیشنز ابھی بھی طے نہیں ہوتی ہیں، وغیرہ۔ یہ سب ایسی خرابیاں پیدا کرتے ہیں جن کا اس سے کوئی تعلق نہیں ہے کہ آیا ایپ واقعی کام کرتی ہے۔

ایک عادت جس نے مجھے سب سے زیادہ مایوس کیا ہے وہ زنجیر نہ باندھنا ہے۔ tester.tap() براہ راست کسی دوسرے تعامل کے بغیر pumpAndSettle() (یا واضح pump(duration)) دریں اثنا، یہاں تک کہ جب یہ بے کار محسوس ہوتا ہے۔

// Flaky: if the tap triggers any async work (a network call, an
// animation), the next find() can run before it resolves,
// and the test fails unpredictably depending on machine speed.
await tester.tap(find.byType(CheckboxListTile).first);
expect(find.textContaining('1 done'), findsOneWidget);

// Reliable: explicitly wait for everything triggered by the tap
// to finish before asserting on the result.
await tester.tap(find.byType(CheckboxListTile).first);
await tester.pumpAndSettle();
expect(find.textContaining('1 done'), findsOneWidget);

خاص طور پر CI کے لیے، حقیقی ڈیوائس فارم کے خلاف انٹیگریشن ٹیسٹ چلانا (Firebase Test Lab یا CI executor سے جڑے ہوئے حقیقی آلات) کیٹگریز کی خرابیوں کو پکڑتا ہے جنہیں ایمولیٹر کبھی کبھی مکمل طور پر کھو سکتے ہیں: اجازت کے ڈائیلاگ جو مختلف طریقے سے برتاؤ کرتے ہیں، کیمرہ یا بائیو میٹرک پرامپٹس، کم میموری جو صرف حقیقی ہارڈ ویئر پر ظاہر ہوتی ہے، ہر رات کو ریلیز کرنے سے پہلے، وغیرہ۔ کیونکہ یہ سب سے مہنگا اور سست ترین آپشن ہے۔

اگر آپ کی ایپ اتنی پیچیدہ ہے کہ بہتر مربوط ٹیسٹ ٹولز کی ضمانت دے سکے (بنیادی اجازتوں کو سنبھالنا، بائیو میٹرکس کا مذاق اڑانا، یا پلیٹ فارم کے گہرے تعاملات) patrol یہ دیکھنے کے قابل ہے۔ اس پر مبنی ہے۔ integration_test تاہم، یہ وہ خصوصیات شامل کرتا ہے جو بیس پیکج میں نہیں پائی جاتی ہیں۔

جہاں ہر ٹیسٹ کی قسم واقعی ادائیگی کرتی ہے۔

اس چار ٹائرڈ اپروچ کے ساتھ متعدد ایپس لانچ کرنے کے بعد، یہاں تقریباً یہ ہے کہ کس طرح اور کیوں کوششیں مختص کی جائیں:

یونٹ ٹیسٹنگ فراخدلی سے کی جاتی ہے۔ آپ اسے تقریباً آزادانہ طور پر لکھ اور چلا سکتے ہیں، اور یہ واحد پرت ہے جو منطقی کیڑے بصری طور پر ظاہر ہونے سے پہلے پکڑتی ہے۔ قیمت کے تمام غیر معمولی حسابات، فلٹرز اور کاروباری منطق ایک حاصل کرتے ہیں۔

ان تمام اسکرینوں پر ویجیٹس کی جانچ کریں جن کی ایک سے زیادہ حالت ہے۔ لوڈنگ، غلطیاں، اور کامیابی تین مختلف کوڈ راستے ہیں، ہر ایک کیڑے کے لیے خاموشی سے چھپانے کی جگہ ہے۔ اگر اسکرین پر صرف ایک ہی حالت ہے تو، ویجیٹ کی جانچ کم قیمت کا اضافہ کرتی ہے۔ اگر آپ کے پاس 3 ہیں، تو ان میں سے 2 کو چھوڑنا اسکرین پر اصل کارروائی کے 2/3 کو چھوڑنے کے مترادف ہے۔

گولڈن ٹیسٹ احتیاط اور جان بوجھ کر کیا جاتا ہے۔ ان اسکرینوں کے لیے مخصوص ہے جہاں بصری رجعت واقعی شرمناک ہو سکتی ہے، جیسے چیک آؤٹ فلو یا کور اسکرینز جو صارفین ہر سیشن میں دیکھتے ہیں۔ میں اسے اپنی ایپ میں ہر اسکرین پر استعمال نہیں کرتا کیونکہ دیکھ بھال کے اخراجات حقیقی ہیں اور ہر ترتیب کو اپنی جگہ پر لاک کرنے کے قابل نہیں ہے۔

متعدد بہاؤ کے لیے انٹیگریشن ٹیسٹ جو ایپ کی وضاحت کرتے ہیں۔ یہ جامع کوریج نہیں ہے، لیکن یہ جاننا کافی ہے کہ ایپ کا اصل مقصد تب بھی کام کرتا ہے جب تمام اصل پرتیں ایک ساتھ منسلک ہوں۔

وہ غلطیاں جو آپ کے ٹیسٹ سویٹ کو آہستہ آہستہ کمزور کر رہی ہیں۔

ان میں سے کوئی بھی تعمیر فوری طور پر نہیں ٹوٹے گا۔ جس طرح ایک غیر ساختہ کوڈبیس ہر ماہ ایک دن کی مکمل ناکامی کے بغیر تبدیلیاں کرنا زیادہ مشکل بناتا ہے، یہ سب آپ کے ٹیسٹ سویٹ کو کم قابل اعتماد بنا دیتے ہیں اگر ہر مہینے کچھ غلط ہو جاتا ہے۔

پہلی غلطی اس کا مذاق اڑانا ہے جس کی آپ اصل میں جانچ کرنا چاہتے ہیں۔ میں نے ذخیروں کے لیے "یونٹ ٹیسٹ” دیکھا اور لکھا ہے جو HTTP کلائنٹس کا بہت اچھی طرح مذاق اڑاتے ہیں۔ ٹیسٹ دراصل یہ ثابت کرنا تھا کہ Dio کے اپنے کلائنٹ نے جیسا سلوک کیا جیسا کہ Dio کی دستاویزات میں بیان کیا گیا ہے۔ اگر آپ کے کوڈ میں کوئی اصل بگ ہے اور ٹیسٹ ناکام نہیں ہو سکتا تو آپ کوڈ کی جانچ نہیں کر رہے ہیں۔

دوسرا ناکامی کے راستے کو چھوڑنا ہے کیونکہ سیٹ اپ تکلیف دہ ہے۔ اس مضمون کی تمام اسکرینوں میں لوڈنگ کی حالت، ایک خرابی کی حالت، اور کامیابی کی حالت ہے، اور ابتدائی طور پر میں نے کئی ٹیموں کو دیکھا، جن میں میری بھی شامل ہے، مکمل طور پر کامیابی کے لیے ویجیٹ ٹیسٹ لکھتے ہیں۔ کیونکہ اسے ترتیب دینا آسان ہے۔ خرابی کی حالتیں اصل کیڑے پر مشتمل ہونے کا سب سے زیادہ امکان ہے کیونکہ یہ وہ راستے ہیں جو ڈویلپر دستی جانچ کے دوران کم سے کم اختیار کرتے ہیں۔

غیر مستحکم ٹیسٹوں کو ایسی چیز سمجھنا بھی ایک غلطی ہے جسے درست کرنے کے بجائے دوبارہ کوشش کرنے کی ضرورت ہے۔ ایک ٹیسٹ جو 20 بار میں ایک بار ناکام ہو جاتا ہے اور دوبارہ چلانے پر پاس ہوتا ہے وہ "کبھی کبھی غیر مستحکم” ٹیسٹ نہیں ہوتا ہے۔ یہ آپ کو آپ کے کوڈ میں ریس کے حالات یا وقت کے بارے میں مفروضوں کی جانچ کے بارے میں کچھ حقیقی بتاتا ہے۔ اگر آپ اسے اپنے CI میں خودکار دوبارہ کوششوں کے ساتھ خاموش کر دیتے ہیں، تو آپ کی پوری ٹیم آپ کے ریڈ بلڈز پر بھروسہ کرنا چھوڑ دے گی۔ یہ غیر مستحکم ٹیسٹ سے کہیں زیادہ مہنگا مسئلہ ہے۔

اور آخر کار، ایسے ٹیسٹ لکھنا ایک غلطی ہے جو رویے کے بجائے عمل درآمد کی تفصیلات کو چیک کرتے ہیں۔ چیک کرنے کے لئے ٹیسٹ notifier._tasks.length (نجی فیلڈ) اس کے بجائے notifier.tasks.length یا، پیش کردہ UI ٹیسٹوں کو اندرونی ڈھانچے سے جوڑتا ہے جن کا کوئی کاروبار براہ راست ٹیسٹ نہیں ہوتا ہے۔ جس لمحے آپ اصل رویے کو تبدیل کیے بغیر داخلی نمائندگی کو ری فیکٹر کریں گے، آپ کے ٹیسٹ ان وجوہات کی بنا پر ٹوٹ جائیں گے جن کا اصل بگ سے کوئی تعلق نہیں ہے۔

سب کچھ ایک ساتھ چلائیں

کوئی راستہ نہیں Makefile متبادل طور پر، ایک CI اسکرپٹ جو چاروں ترتیب سے چلتا ہے (سب سے سستا پہلے) شروع ہونے سے پہلے ہی سب سے مہنگے مسائل کو پکڑ لے گا۔

# Fails fast on logic bugs before spending time on anything else.
flutter test test/task_logic_test.dart

# Widget-level behavior across all three UI states.
flutter test test/task_screen_test.dart

# Visual regressions on the screens that matter.
flutter test test/task_screen_golden_test.dart

# The real thing, on a real device or emulator — last, because it's slowest.
flutter test integration_test/complete_task_test.dart

CI ہر پل کی درخواست کے لیے پہلے تین چلاتا ہے۔ وہ اتنے تیز ہیں کہ کوئی عذر نہیں ہے۔ انٹیگریٹڈ سویٹس کو اپنے مین یا رات کے شیڈول میں ضم کرکے چلائیں۔ چونکہ اس کے لیے ایک حقیقی ڈیوائس یا ایمولیٹر کی ضرورت ہوتی ہے اور تمام PRs کو بلاک کرنے کے لیے کافی وقت لگتا ہے، اس لیے یہ ٹیم کو ان کیٹیگریز کے کیڑے کے لیے سست کر دیتا ہے جنہیں ایک اچھا ویجیٹ ٹیسٹنگ سوٹ پہلے ہی زیادہ تر وقت پکڑتا ہے۔

آخر سے آخر تک: ایک CI پائپ لائن میں چاروں پرتیں۔

یہاں یہ ہے کہ اسے حقیقی پروجیکٹ میں GitHub ایکشنز سے کیسے جوڑنا ہے: پہلے ایک فوری جانچ پڑتال کی جاتی ہے، لہذا اگر کوئی مرحلہ ناکام ہوجاتا ہے، تو اگلے مرحلے پر وقت ضائع ہونے سے پہلے پائپ لائن کو روک دیا جاتا ہے۔

# .github/workflows/test.yml
name: Test

on: [pull_request, push]

jobs:
  unit-and-widget:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      # Unit and widget tests together — both fast, both run on
      # every PR without a second thought about cost.
      - run: flutter test test/task_logic_test.dart test/task_screen_test.dart

  golden:
    runs-on: ubuntu-latest
    needs: unit-and-widget
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      - run: flutter test test/task_screen_golden_test.dart
      # Upload diffs so a reviewer can see exactly what changed
      # without pulling the branch locally — this is what keeps
      # golden failures from becoming a rubber-stamped --update-goldens.
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: golden-diffs
          path: test/failures/

  integration:
    runs-on: macos-latest # needed for iOS simulator; use ubuntu + Android emulator otherwise
    needs: golden
    if: github.ref == 'refs/heads/main' # only on merges to main, not every PR
    steps:
      - uses: actions/checkout@v4
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      - run: flutter test integration_test/complete_task_test.dart -d "iPhone 15"

کہ needs: زنجیر اور if: انضمام کے کام کے حالات وہ ہیں جہاں حقیقی کام کیا جاتا ہے۔ یہ سب سے سست اور مہنگے چیکوں کو ہر ایک پش پر چلنے سے روکتا ہے، جبکہ اس بات کو بھی یقینی بناتا ہے کہ وہ کسی بھی چیز کے پہنچنے سے پہلے ہی چلتے ہیں۔ main.

حتمی خیالات

میں "ٹیسٹ” کے بارے میں ایک واحد آئٹم کے طور پر سوچتا تھا، جو کچھ کیا گیا تھا یا نہیں کیا گیا تھا، ڈیش بورڈ کا ایک فیصد۔ یہ سچ نہیں ہے۔ یونٹ ٹیسٹ آپ کی منطق کی حفاظت کرتے ہیں۔ ویجیٹ ٹیسٹ ریاست کی حفاظت کرتے ہیں۔ گولڈن ٹیسٹ آپ کے پکسلز کی حفاظت کرتا ہے۔ انٹیگریشن ٹیسٹنگ اس وعدے کی حفاظت کرتی ہے کہ اصل میں ایک حقیقی ڈیوائس پر سب کچھ مل کر کام کرے گا۔

ایک بار جب آپ اصل فعالیت قائم کر لیتے ہیں تو ان میں سے کوئی بھی لکھنا مشکل نہیں ہوتا۔ اسی لیے میں نے یہ مضمون چار منقطع ٹکڑوں کی بجائے ایک خصوصیت کے گرد لکھا ہے۔ کام کی تکمیل کا بگ جس نے اس مضمون کو شروع کیا (ایک کام جس کو غلط فہرست میں مکمل کے طور پر نشان زد کیا گیا ہے) شاید ایک یونٹ ٹیسٹ میں دریافت کیا گیا تھا۔ markDoneویجیٹ پرت کو چھونے سے پہلے لکھا گیا۔

میں اس ٹیسٹ کو لکھنے والا پہلا نہیں تھا۔ اب میں اس اور اس کے بہن بھائیوں کی تمام اہم خصوصیات کے بارے میں لکھ رہا ہوں۔ یہ اصل میں پورا سبق ہے۔ ایسا نہیں ہے کہ ٹیسٹ پیچیدہ ہیں، یہ ہے کہ چار اقسام میں سے ہر ایک ایسے سوالات کے جوابات دیتا ہے جو دیگر تین اقسام نہیں دے سکتے۔

اوپر تک سکرول کریں۔