felangel / mocktail

A mock library for Dart inspired by mockito
https://pub.dev/packages/mocktail
MIT License
617 stars 81 forks source link

Verifying args callbacks always fails #114

Closed mrgnhnt96 closed 2 years ago

mrgnhnt96 commented 2 years ago

Describe the bug When passing a void Function() as an arg, verify() seems to always fail.

Code to Reproduce

class Example {
  const Example();

  void run(void Function() callback) {
    callback();
  }
}

class Methods {
  const Methods();

  void foo() {}
  void bar() {}
}

class MockMethods extends Mock implements Methods {}

class MockExample extends Mock implements Example {}

extension on Methods {
  void baz() {}
}

void runner({
  required Example example,
  required Methods methods,
}) {
  example
    ..run(methods.foo)
    ..run(methods.bar)
    ..run(methods.baz);
}

void main() {
  late Example mockExample;
  late Methods mockMethods, methods;

  setUp(() {
    mockExample = MockExample();
    mockMethods = MockMethods();
    methods = const Methods();
  });

  test('gracefully runs mocked callback provided to mock #run', () async {
    runner(example: mockExample, methods: mockMethods);

    verify(() => mockExample.run(any())).called(3); // passes
    verify(() => mockExample.run(mockMethods.foo)).called(1); // fails
    verify(() => mockExample.run(mockMethods.bar)).called(1); // fails
    verify(() => mockExample.run(mockMethods.baz)).called(1); // fails
  });

  test('gracefully runs real callback provided to mock #run', () async {
    runner(example: mockExample, methods: methods);

    verify(() => mockExample.run(any())).called(3); // passes
    verify(() => mockExample.run(methods.foo)).called(1); // fails
    verify(() => mockExample.run(methods.bar)).called(1); // fails
    verify(() => mockExample.run(methods.baz)).called(1); // fails
  });
}

Logs

gracefully runs mocked callback provided to mock #run ```console No matching calls. All calls: [VERIFIED] MockExample.run(Closure: () => void from Function 'foo':.), [VERIFIED] MockExample.run(Closure: () => void from Function 'bar':.), [VERIFIED] MockExample.run(Closure: () => void) (If you called `verify(...).called(0);`, please instead use `verifyNever(...);`.) package:test_api fail package:mocktail/src/mocktail.dart 723:7 _VerifyCall._checkWith package:mocktail/src/mocktail.dart 516:18 _makeVerify. test/utils/fake_test.dart 51:11 main. test/utils/fake_test.dart 47:65 main. ```
gracefully runs real callback provided to mock #run ```console No matching calls. All calls: [VERIFIED] MockExample.run(Closure: () => void from Function 'foo':.), [VERIFIED] MockExample.run(Closure: () => void from Function 'bar':.), [VERIFIED] MockExample.run(Closure: () => void) (If you called `verify(...).called(0);`, please instead use `verifyNever(...);`.) package:test_api fail package:mocktail/src/mocktail.dart 723:7 _VerifyCall._checkWith package:mocktail/src/mocktail.dart 516:18 _makeVerify. test/utils/fake_test.dart 60:11 main. test/utils/fake_test.dart 56:63 main. ```
flutter doctor ```console [✓] Flutter (Channel stable, 2.10.1, on macOS 12.1 21C52 darwin-arm, locale en-US) • Flutter version 2.10.1 at /Users/morganhunt/Development/versions/stable • Upstream repository https://github.com/flutter/flutter.git • Framework revision db747aa133 (13 days ago), 2022-02-09 13:57:35 -0600 • Engine revision ab46186b24 • Dart version 2.16.1 • DevTools version 2.9.2 [!] Android toolchain - develop for Android devices (Android SDK version 29.0.3) • Android SDK at /Users/morganhunt/Library/Android/sdk ✗ cmdline-tools component is missing Run `path/to/sdkmanager --install "cmdline-tools;latest"` See https://developer.android.com/studio/command-line for more details. ✗ Android license status unknown. Run `flutter doctor --android-licenses` to accept the SDK licenses. See https://flutter.dev/docs/get-started/install/macos#android-setup for more details. [✓] Xcode - develop for iOS and macOS (Xcode 13.2.1) • Xcode at /Applications/Xcode.app/Contents/Developer • CocoaPods version 1.11.2 [✓] Chrome - develop for the web • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome [✓] Android Studio (version 4.1) • Android Studio at /Applications/Android Studio.app/Contents • Flutter plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/9212-flutter • Dart plugin can be installed from: 🔨 https://plugins.jetbrains.com/plugin/6351-dart • Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495) [✓] VS Code (version 1.64.2) • VS Code at /Applications/Visual Studio Code.app/Contents • Flutter extension version 3.34.0 [✓] Connected device (1 available) • Chrome (web) • chrome • web-javascript • Google Chrome 98.0.4758.102 ! Error: iPhone is not connected. Xcode will continue when iPhone is connected. (code -13) [✓] HTTP Host Availability • All required HTTP hosts are available ! Doctor found issues in 1 category. ```
felangel commented 2 years ago

Hi @mrgnhnt96 👋 Thanks for opening an issue!

I believe the problem is you are verifying run(any()).called(3) first which verifies the calls successfully so they are no longer considered in future verifications.

The following passes locally

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

class Example {
  const Example();

  void run(void Function() callback) {
    callback();
  }
}

class Methods {
  const Methods();

  void foo() {}
  void bar() {}
  void baz() {}
}

class MockMethods extends Mock implements Methods {}

class MockExample extends Mock implements Example {}

void runner({
  required Example example,
  required Methods methods,
}) {
  example
    ..run(methods.foo)
    ..run(methods.bar)
    ..run(methods.baz);
}

void main() {
  late Example mockExample;
  late Methods mockMethods, methods;

  setUp(() {
    mockExample = MockExample();
    mockMethods = MockMethods();
    methods = const Methods();
  });

  test('gracefully runs mocked callback provided to mock #run', () async {
    runner(example: mockExample, methods: mockMethods);

    verify(() => mockExample.run(mockMethods.foo)).called(1);
    verify(() => mockExample.run(mockMethods.bar)).called(1);
    verify(() => mockExample.run(mockMethods.baz)).called(1);
  });

  test('gracefully runs real callback provided to mock #run', () async {
    runner(example: mockExample, methods: methods);

    verify(() => mockExample.run(methods.foo)).called(1);
    verify(() => mockExample.run(methods.bar)).called(1);
    verify(() => mockExample.run(methods.baz)).called(1);
  });
}
mrgnhnt96 commented 2 years ago

Interesting, I didn't know that they were taken from the running after they were verified. Maybe I overlooked it, thanks for letting me know!

I know extensions can't really be verified. But what if I want to verify what happens in the extension method? In my case I am using mason_logger, I have created an extension method called "cooking" which calls info('cooking...'), (in an effort to stay DRY). I don't necessarily care that cooking is called, but I do care that info('cooking...') is called. I think that the tests still fail though

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

class Example {
  const Example();

  void run(void Function() callback) {
    callback();
  }
}

class Methods {
  const Methods();

  void foo() {}
  void bar() {}
}

extension on Methods {
  void baz() {
    foo();
    bar();
  }
}

class MockMethods extends Mock implements Methods {}

class MockExample extends Mock implements Example {}

void runner({
  required Example example,
  required Methods methods,
}) {
  example
    ..run(methods.foo)
    ..run(methods.bar)
    ..run(methods.baz);
}

void main() {
  late Example mockExample;
  late Methods mockMethods, methods;

  setUp(() {
    mockExample = MockExample();
    mockMethods = MockMethods();
    methods = const Methods();
  });

  test('gracefully runs mocked callback provided to mock #run', () async {
    runner(example: mockExample, methods: mockMethods);

    verify(() => mockExample.run(mockMethods.foo)).called(2); // Expected: <2> Actual: <1>
    verify(() => mockExample.run(mockMethods.bar)).called(2); // Expected: <2> Actual: <1>
  });

  test('gracefully runs real callback provided to mock #run', () async {
    runner(example: mockExample, methods: methods);

    verify(() => mockExample.run(methods.foo)).called(2); // Expected: <2> Actual: <1>
    verify(() => mockExample.run(methods.bar)).called(2); // Expected: <2> Actual: <1>
  });
}
felangel commented 2 years ago

@mrgnhnt96 not sure why you expect those methods to be called twice when runner is only called once. If you want them to be called twice you'd need to call runner twice like:

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

class Example {
  const Example();

  void run(void Function() callback) {
    callback();
  }
}

class Methods {
  const Methods();

  void foo() {}
  void bar() {}
}

extension on Methods {
  void baz() {
    foo();
    bar();
  }
}

class MockMethods extends Mock implements Methods {}

class MockExample extends Mock implements Example {}

void runner({
  required Example example,
  required Methods methods,
}) {
  example
    ..run(methods.foo)
    ..run(methods.bar)
    ..run(methods.baz);
}

void main() {
  late Example mockExample;
  late Methods mockMethods, methods;

  setUp(() {
    mockExample = MockExample();
    mockMethods = MockMethods();
    methods = const Methods();
  });

  test('gracefully runs mocked callback provided to mock #run', () async {
    runner(example: mockExample, methods: mockMethods);
    runner(example: mockExample, methods: mockMethods);

    verify(() => mockExample.run(mockMethods.foo)).called(2);
    verify(() => mockExample.run(mockMethods.bar)).called(2);
  });

  test('gracefully runs real callback provided to mock #run', () async {
    runner(example: mockExample, methods: methods);
    runner(example: mockExample, methods: methods);

    verify(() => mockExample.run(methods.foo)).called(2);
    verify(() => mockExample.run(methods.bar)).called(2);
  });
}

Let me know if that helps or if I missed something. Closing for now but I'm happy to continue the conversation 👍