Sparround

Fake vs mock: mocktail ilə praktika

Rəsmi tövsiyələr "test üçün fake-lər yazın" bəndini ən yüksək prioritetdə verir və səbəbi konkretdir: fake obyektin daxili işini deyil, giriş və çıxışını yoxlayır — bu isə modul, yaxşı təyin olunmuş interfeyslər yazmağa məcbur edir.

Fərqi qısa formalaşdırmaq olar:

  • Fake — müqavilənin sadələşdirilmiş, lakin işlək implementasiyası. add çağırıldıqda element həqiqətən əlavə olunur; siyahı yaddaşda saxlanılır. Bir dəfə yazılır, onlarla testdə işlədilir.
  • Mock — davranışı olmayan, yalnız cavab verməyə proqramlaşdırılmış obyekt. Hər testdə when(() => ...) quraşdırması yazılır və verify(...) ilə çağırışlar yoxlanılır.

Seçim meyarı bir sualdır: "nəyi yoxlayıram — nəticəni, yoxsa çağırışı?"

  • Nəticə (səbətdə 3 element var, yekun 32 manatdır, status cancelled oldu) → fake.
  • Çağırış (analitika hadisəsi göndərildi, cache invalidate olundu, logout bir dəfə çağırıldı) → mock.

Dart-da mock üçün əsas alət mocktail-dır: kod generasiyası tələb etmir, MockCat extends Mock implements Cat kimi elan olunur və çağırışlar closure ilə yazılır — when(() => cat.sound()).thenReturn('meow').

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

// 1) Elan: kod generasiyası YOXDUR, `build_runner` lazım deyil.
class MockAnalyticsService extends Mock implements AnalyticsService {}
class MockOrderRepository extends Mock implements OrderRepository {}

// 2) Öz tiplərin `any()` ilə işlədilirsə, fallback qeyd olunmalıdır.
class _FakeOrder extends Fake implements Order {}

void main() {
  setUpAll(() {
    // `any()` matcher-ı işlətməzdən əvvəl bir dəfə çağırılır.
    registerFallbackValue(_FakeOrder());
  });

  late MockAnalyticsService analytics;

  setUp(() {
    analytics = MockAnalyticsService();
    // Void metodlar üçün: thenAnswer((_) async {})
    when(() => analytics.track(any(), properties: any(named: 'properties')))
        .thenAnswer((_) async {});
  });

  test('sifariş verildikdə analitika hadisəsi göndərilir', () async {
    final notifier = CheckoutNotifier(
      repository: FakeOrderRepository([]),   // NƏTİCƏ üçün fake
      analytics: analytics,                  // ÇAĞIRIŞ üçün mock
    );

    await notifier.submit(testForm);

    // Çağırışın yoxlanılması — burada mock haqlıdır,
    // çünki "nəticə" yoxdur: hadisə göndərildi, vəssalam.
    verify(() => analytics.track('order_placed',
        properties: any(named: 'properties'))).called(1);
  });

  test('xəta halında analitika hadisəsi göndərilmir', () async {
    final notifier = CheckoutNotifier(
      repository: FakeOrderRepository([], placeError: const NetworkFailure()),
      analytics: analytics,
    );

    await notifier.submit(testForm);

    verifyNever(() => analytics.track('order_placed',
        properties: any(named: 'properties')));
  });

  test('stub-ın qaytardığı dəyər', () {
    final repo = MockOrderRepository();

    // Sinxron dəyər üçün thenReturn, Future üçün thenAnswer.
    when(() => repo.fetchMine())
        .thenAnswer((_) async => Result.ok(const <Order>[]));

    // Arqumentə görə fərqli cavab:
    when(() => repo.cancel('o-1'))
        .thenAnswer((_) async => Result.ok(cancelledOrder));
    when(() => repo.cancel('o-2'))
        .thenAnswer((_) async => const Result.error(NotFoundFailure()));

    // Xəta atmaq:
    when(() => repo.fetchById(any())).thenThrow(const NetworkFailure());
  });
}

// mocktail-in mockito-dan əsas fərqləri:
//   • kod generasiyası yoxdur (@GenerateMocks, build_runner lazım deyil)
//   • çağırışlar closure-a bükülür: when(() => ...), verify(() => ...)
//   • tip-spesifik matcher-lar (anyString, anyInt) yerinə vahid `any()`,
//     `any(named: '...')`, `any(that: ...)`

mocktail-ın əsas API-si: elan, stub, yoxlama və `registerFallbackValue`.

MeyarFakeMock (mocktail)
Nə yoxlanılırNəticə (state, qaytarılan dəyər)Çağırışın baş verməsi
Yazılma qiymətiBir dəfə, 20-40 sətirBir sətir elan, lakin hər testdə `when` quraşdırması
Təkrar istifadəYüksək — bütün testlərdəAşağı — quraşdırma testə xasdır
Refaktora həssaslıqAşağı — müqavilə dəyişməsə işləyirYüksək — metod adı/parametr dəyişdikdə sınır
Real davranışVar (yaddaşda state saxlayır)Yoxdur (yalnız cavab verir)
Ən uyğun yerRepository, service, use-case müqavilələriAnalitika, loqlama, bildiriş — yan effektlər

Yaxşı fake-in üç xüsusiyyəti.

1. İşlək state. Yaddaşda siyahı ya da Map saxlayır; add, update, delete real təsir edir. Bu, testin "iki dəfə əlavə etdikdə say artır" kimi davranışları yoxlamasına imkan verir. 2. Xəta inyeksiyası. Konstruktor parametri ilə xəta yolunu açmaq: FakeOrderRepository(const [], fetchError: NetworkFailure()). Belə olduqda hər xəta halı üçün ayrı fake sinfi lazım gəlmir. 3. Sayğaclar. fetchCount, cancelCount — cache və birləşdirmə davranışını yoxlamaq üçün. Bu, mock-un verify-ına oxşar imkan verir, lakin fake-in davranışını itirmədən.

Nə vaxt gecikmə əlavə etmək. Test fake-ində gecikmə olmamalıdır — test sürətli olsun. Gecikmə yalnız iki halda lazımdır: (1) ...Local implementasiyasında (əvvəlki mərhələ — loading state-ini gözlə görmək üçün), (2) konkret bir testdə yarış vəziyyətini yoxlamaq üçün (placeDelay parametri ilə).

Fixture-lar (test məlumatı). Hər testdə Order(id: 'o-1', title: ..., status: ..., total: ...) yazmaq təkrardır və dəyişikliyə həssasdır: modelə yeni məcburi sahə əlavə etdikdə 40 test faylı sınır. Həlli — test/fixtures/ qovluğunda builder funksiyaları: anOrder(status: OrderStatus.pending). Yeni sahə əlavə olunduqda yalnız builder dəyişir.

mocktail-ın tələsi. any() matcher-ı öz tiplərinlə işlədilirsə, registerFallbackValue çağırılmalıdır — əks halda test icra vaxtı xəta verir. Bunu setUpAll-da bir dəfə etmək kifayətdir.

Praktika. İki addım:

1. test/fixtures/order_fixtures.dart faylı yarat: Order anOrder({String? id, OrderStatus? status, double? total}) builder-i — bütün sahələrin defolt dəyəri olsun. 2. Bir repository müqaviləsi üçün tam fake yaz: işlək state + xəta inyeksiyası (konstruktor parametri) + iki sayğac. Sonra onu ən azı üç testdə işlət: notifier testi, widget testi və xəta yolu testi.

Hazır sayılır: modelə yeni sahə əlavə etdikdə yalnız builder faylı dəyişir (testlər sınmır), və xəta halını yoxlamaq üçün yeni sinif yazmaq lazım gəlmir.

📚 Mənbələr və sənədlər

  • mocktail paketirəsmipub.dev

    `when(() => ...)`, `verify(() => ...)`, `any()`, `registerFallbackValue` — bu mövzudaki API-nin mənbəyi.

  • Arxitektura tövsiyələri: fake-lərrəsmidocs.flutter.dev

    "Test üçün fake-lər yazın" tövsiyəsi və onun əsaslandırması.

  • Case study: hər qatın testirəsmidocs.flutter.dev

    Real layihədə `FakeBookingRepository`, `FakeApiClient` kimi fake-lərin necə yazıldığı.

  • Flutter: unit testlərrəsmidocs.flutter.dev

    `setUp`, `setUpAll`, `group` və test strukturunun əsasları.