mocktail

repository·main·Indexed 20 days ago

https://github.com/felangel/mocktail

A null-safe Dart mocking library that eliminates the need for code generation. It provides a simplified API for stubbing and verifying behavior using when() and verify(), and includes the mocktail_image_network package to mock Image.network in Flutter widget tests.

Tokens
5.2K
Snippets
19
Records
25
Agent score
71%

What's inside mocktail

  1. Overview of mocktail libraries

    main

    The mocktail repository provides Dart mocking libraries inspired by mockito. It consists of two primary packages:

    1. mocktail: A mocking library designed for Dart with null safety support. It simplifies the mocking process by eliminating the need for manual mocks or code generation.
    2. mocktail_image_network: A specialized package used to mock Image.network within widget tests, built to work seamlessly with package:mocktail.
  2. Why use mocktail_image_network for Image.network

    main

    By default, Flutter's TestWidgetsFlutterBinding intercepts all HTTP requests and returns a status code 400. If your widget uses Image.network, the image resource service will throw a NetworkImageLoadException because it cannot reach the URL.

    mocktail_image_network provides a controlled environment by mocking the internal HTTP client, preventing these exceptions and allowing your widget tests to pass even when they contain network-dependent images.

  3. Mock Image.network in widget tests with mocktail_image_network

    main
    If your widget tests rely on Image.network, use package:mocktail_image_network. This package allows you to mock network images with confidence using the core mocktail API.
  4. Register fallback values for custom types

    main

    When using argument matchers like any() or captureAny() with custom types, mocktail needs a default value to return to avoid type errors. You must register a fallback value for each custom type using registerFallbackValue. It is recommended to do this once per type in a setUpAll block.

    class Food {...}
    class Cat {
      bool likes(Food food) => true;
    }
    
    class MockCat extends Mock implements Cat {}
    class FakeFood extends Fake implements Food {}
    
    void main() {
      setUpAll(() {
        registerFallbackValue(FakeFood());
      });
    
      test('...', () {
        final cat = MockCat();
        when(() => cat.likes(any())).thenReturn(true);
      });
    }
  5. Use package:mocktail for null-safe mocking without code generation

    main
    Use package:mocktail when you need a Dart mocking library that supports null safety and does not require a code generation step (like build_runner). This makes it ideal for rapid development and simpler test setups.
  6. Migrate from Mockito to Mocktail

    main

    Mocktail is designed to be a drop-in replacement for Mockito with a similar API, but it eliminates the need for code generation. When migrating, keep these three key differences in mind:

    1. No code generation: Remove @GenerateMocks, build_runner, and any generated .mocks.dart files. Instead, manually create mock classes by extending Mock and implementing the interface.
    2. Wrap calls in closures: Unlike Mockito, where you pass the method call directly to when() or verify(), Mocktail requires you to wrap the call in a closure: () => mock.method().
    3. Unified matchers: Use unified matchers like any(), any(named: 'param'), and any(that: matcher) instead of type-specific matchers like anyString or argThat.
  7. Create a Mock in mocktail

    main

    To create a mock, define a class that extends Mock and implements the interface of the class you want to mock. This approach avoids the need for code generation.

    import 'package:mocktail/mocktail.dart';
    
    // A Real Cat class
    class Cat {
      String sound() => 'meow!';
      bool likes(String food, {bool isHungry = false}) => false;
      final int lives = 9;
    }
    
    // A Mock Cat class
    class MockCat extends Mock implements Cat {}
    
    void main() {
      // Create a Mock Cat instance
      final cat = MockCat();
    }
  8. Mock Image.network in widget tests with mockNetworkImages

    main

    When testing Flutter widgets that use Image.network, tests will fail because the TestWidgetsFlutterBinding prevents real network requests, resulting in a NetworkImageLoadException (HTTP 400).

    To resolve this, wrap your test logic inside the mockNetworkImages function. This mocks the internal HTTP client used by Image.network, allowing the images to load successfully during the test without making actual network calls.

    void main() {
      testWidgets('can use mocktail for network images', (tester) async {
        // Wrap the widget pump in mockNetworkImages
        await mockNetworkImages(() async => tester.pumpWidget(FakeApp()));
        
        expect(find.byType(Image), findsOneWidget);
      });
    }
  9. Troubleshoot common mocktail errors

    main

    Type 'Null' is not a subtype of type 'Future<void>'

    This occurs when a non-nullable method (like one returning Future<void>) is called on a mock but has not been stubbed. Because unstubbed methods return null by default, a type mismatch occurs. Fix: Explicitly stub the method: when(() => mock.method()).thenAnswer((_) async {});

    Method throwing TypeError when using any()

    If a method uses generic type arguments (e.g., set<T>(String key, T value)), any() might infer dynamic, causing the stub to fail. Fix: Explicitly provide the type to the stub: when(() => cache.set<int>(any(), any())).thenReturn(...) or when(() => cache.set(any(), any<int>())).

    Extension methods cannot be stubbed

    Extension methods are treated like static methods and cannot be intercepted by mocktail. Fix: Stub or verify the public members of the instance that the extension method operates on instead.

  10. Sample Migration: Mockito to Mocktail

    main

    This example demonstrates the transition from a code-generated Mockito setup to a manual Mocktail setup.

    // After (mocktail)
    import 'package:mocktail/mocktail.dart';
    
    class MockCat extends Mock implements Cat {}
    
    void main() {
      test('sounds', () {
        final Cat cat = MockCat();
        when(() => cat.sound()).thenReturn('meow');
        expect(cat.sound(), 'meow');
        verify(() => cat.sound()).called(1);
      });
    
      test('eat with matcher', () {
        final Cat cat = MockCat();
        when(() => cat.eat(any(that: startsWith('fish')))).thenAnswer((_) async => 'yum');
        expect(cat.eat('fishy'), completion(equals('yum')));
        verify(() => cat.eat('fishy')).called(1);
      });
    }
  11. Use argument matchers like any() and captureAny()

    main

    Mocktail provides matchers to stub or verify methods based on argument patterns rather than exact values.

    • any(): Matches any value for a positional argument.
    • any(named: 'argName'): Matches any value for a named argument.
    • any(that: matcher): Uses a custom matcher (e.g., isA<T>()).
    • captureAny(): Used within verify() to capture the arguments passed to a method for later inspection.
    • captureAny(that: matcher): Captures arguments that match a specific criteria.
    // Stubbing with matchers
    when(() => cat.likes(any(), isHungry: any(named: 'isHungry', that: isFalse))).thenReturn(true);
    
    // Verifying with matchers
    verify(() => cat.likes(any(that: isA<String>().having((food) => food, 'name', 'fish')))).called(1);
    
    // Capturing arguments
    when(() => cat.likes('fish')).thenReturn(true);
    cat.likes('fish');
    final captured = verify(() => cat.likes(captureAny())).captured;
    expect(captured.last, equals(['fish']));