juliansteenbakker / mobile_scanner

A universal scanner for Flutter based on MLKit. Uses CameraX on Android and AVFoundation on iOS.
BSD 3-Clause "New" or "Revised" License
756 stars 446 forks source link

Golden tests error #732

Closed Andre-Coelhoo closed 3 months ago

Andre-Coelhoo commented 9 months ago

Hello everyone, I doing golden tests and Im getting multiple errors, this is the widget: (im using the version 3.0.0) `class ScanWidget extends StatelessWidget { const ScanWidget({Key? key}) : super(key: key);

@override Widget build(BuildContext context) { final scanRectSize = MediaQuery.of(context).size.width * 0.6;

final mobileScannerController = MobileScannerController(
  facing: CameraFacing.back,
  torchEnabled: false,
);

return Stack(
  alignment: Alignment.center,
  children: [
    MobileScanner(
      controller: mobileScannerController,
      onDetect: (capture) async {
        await mobileScannerController.stop();
        final result = await context
            .read<TagOnBoardingCubit>()
            .checkScannedCode(capture.barcodes.first);
        if (result != null) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text(result),
            ),
          );
          await Future.delayed(Duration(seconds: 2));
          mobileScannerController.start();
        }
      },
    ),
    Positioned.fill(
      child: Container(
        decoration: ShapeDecoration(
          shape: ScanOverlayShape(
            borderColor: Colors.white,
            borderRadius: 10,
            borderLength: scanRectSize / 4,
            borderWidth: 5,
            cutOutSize: scanRectSize,
          ),
        ),
      ),
    ),
    Positioned(
      left: MediaQuery.of(context).size.width * 0.05,
      top: MediaQuery.of(context).size.height * 0.1,
      child: IconButton(
        padding: EdgeInsets.zero,
        constraints: BoxConstraints(),
        icon: const Icon(Icons.close, color: Colors.white),
        onPressed: () => Navigator.of(context).pop(),
      ),
    ),
    Positioned(
      top: MediaQuery.of(context).size.height * 0.1,
      child: Row(
        children: [
          Text(
            S.of(context).scanTag.toUpperCase(),
            style: tagOnBoardingBoldText.copyWith(color: Colors.white),
          ),
        ],
      ),
    ),
    Positioned(
      top: MediaQuery.of(context).size.height * 0.16,
      child: Row(
        children: [
          /// Scan button
          Container(
            padding: EdgeInsets.all(8),
            decoration: BoxDecoration(
              borderRadius: BorderRadius.all(Radius.circular(6)),
              color: Colors.white,
            ),
            child: Column(
              children: [
                tagOnBoardingQrCodeAsset,
                SizedBox(height: 8),
                Text(
                  S.of(context).scan.toUpperCase(),
                  style: tagOnBoardingSmallText.copyWith(
                      color: secondaryTextColor),
                ),
              ],
            ),
          ),
          SizedBox(width: MediaQuery.of(context).size.height * 0.05),

          ///Nfc Button
          InkWell(
            onTap: () =>
                BlocProvider.of<TagOnBoardingCubit>(context).changeToNfc(),
            child: Container(
              padding: EdgeInsets.all(8),
              child: Column(
                children: [
                  tagOnBoardingContactlessAsset,
                  SizedBox(height: 8),
                  Text(
                    S.of(context).nfc.toUpperCase(),
                    style: tagOnBoardingSmallText.copyWith(
                        color: Colors.white),
                  ),
                ],
              ),
            ),
          ),
          SizedBox(width: MediaQuery.of(context).size.height * 0.05),

          /// Type button
          InkWell(
            onTap: () =>
                BlocProvider.of<TagOnBoardingCubit>(context).changeToType(),
            child: Container(
              padding: EdgeInsets.all(8),
              child: Column(
                children: [
                  tagOnBoardingNumPadAsset,
                  SizedBox(height: 8),
                  Text(
                    S.of(context).type.toUpperCase(),
                    style: tagOnBoardingSmallText.copyWith(
                        color: Colors.white),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    ),
  ],
);

} } `

this is my golden test `void main() { final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();

group('scan tagOnboarding screen Golden Test', () { for (GoldenTheme theme in GoldenTheme.values) { for (GoldenScale scale in GoldenScale.values) { for (PhysicalSize physicalSizeDevices in PhysicalSize.values) { testWidgets('scan tagOnboarding page', (WidgetTester tester) async { // change screen size and pixel density ratio binding.window.physicalSizeTestValue = physicalSizeDevices.physicalSizes; binding.window.devicePixelRatioTestValue = 1.0;

        await loadAppFonts();
        final MockSessionCubit sessionCubit = MockSessionCubit();

        // create the mocks
        // use them in the sessionCubit stubbing
        await pumpWithSettle(
          tester,
          GoldenApp(
            theme: theme,
            scale: scale,
            child: BlocProvider<SessionCubit>(
              create: (context) => sessionCubit,
              child: ScanWidget(),

            ),
          ),
        );

        await expectLater(
          find.byType(ScanWidget),
          matchesGoldenFile(
            'goldens/notifications_${scale.name}_${theme.name}_${physicalSizeDevices.name}.png',
          ),
        );
      });
    }
  }
}

}); } `

I get this errors:

`The following _CastError was thrown running a test: type 'MissingPluginException' is not a subtype of type 'MobileScannerException' in type cast

When the exception was thrown, this was the stack:

0 _MobileScannerState._startScanner.. (package:mobile_scanner/src/mobile_scanner.dart:138:35)

1 State.setState (package:flutter/src/widgets/framework.dart:1133:30)

2 _MobileScannerState._startScanner. (package:mobile_scanner/src/mobile_scanner.dart:137:9)

(elided 24 frames from dart:async and package:stack_trace)` `══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following assertion was thrown running a test (but after the test had completed): 'package:flutter_test/src/binding.dart': Failed assertion: line 1146 pos 12: 'inTest': is not true. When the exception was thrown, this was the stack: #2 AutomatedTestWidgetsFlutterBinding.clock (package:flutter_test/src/binding.dart:1146:12) #3 WidgetTester.pumpAndSettle. (package:flutter_test/src/widget_tester.dart:673:40) #6 TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:71:41) #7 WidgetTester.pumpAndSettle (package:flutter_test/src/widget_tester.dart:672:27) #8 pumpWithSettle (file:///Users/andrecoelho/Desktop/projectToPrinter/mobile-app/test/custom_widget_pump.dart:39:18) (elided 5 frames from class _AssertionError, dart:async, and package:stack_trace)` `══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════ The following MissingPluginException was thrown running a test: MissingPluginException(No implementation found for method updateScanWindow on channel dev.steenbakker.mobile_scanner/scanner/method) When the exception was thrown, this was the stack: #0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:313:7) (elided one frame from package:stack_trace) The test description was: scan tagOnboarding page`
Timbwa commented 9 months ago

AFAIK the method channels methods need to be overridden in tests when working with plugins. I wanted to post an issue, asking about how to go about testing with Mobile Scanner, maybe an elaboration on which methods to override safely etc. But seeing the many issues I don't know if the author or maintainers will get to them in time

Andre-Coelhoo commented 9 months ago

AFAIK the method channels methods need to be overridden in tests when working with plugins. I wanted to post an issue, asking about how to go about testing with Mobile Scanner, maybe an elaboration on which methods to override safely etc. But seeing the many issues I don't know if the author or maintainers will get to them in time

but how do I override the function of a package?

navaronbracke commented 9 months ago

You indeed need to use https://api.flutter.dev/flutter/flutter_test/TestDefaultBinaryMessenger/setMockMethodCallHandler.html to mock method calls in tests

navaronbracke commented 9 months ago

Before I forget, there is also https://docs.flutter.dev/testing/plugins-in-tests which explains the setup, and why it fails in your case.

I am planning on rewriting part of the plugin to use the plugin_platform_interface, as that is the way to develop plugins today. This will give you the option to register a mock implementation of the platform interface (see the doc link above)

Andre-Coelhoo commented 9 months ago

Hello again, I'm trying to use setMockMethodCallHandler. I made this function that runs before the test

void mockMobileScanner() {
  const channel = MethodChannel('mobile_scanner/mobile_scanner.dart');
  handler(MethodCall methodCall) async {
    if (methodCall.method == '_startScanner') {
      return null;
    }

    return null;
  }

  TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
      .setMockMethodCallHandler(channel, handler);
}

but still got this error: The following _CastError was thrown running a test (but after the test had completed): type 'MissingPluginException' is not a subtype of type 'MobileScannerException' in type cast

When the exception was thrown, this was the stack:

0 _MobileScannerState._startScanner.. (package:mobile_scanner/src/mobile_scanner.dart:138:35)

1 State.setState (package:flutter/src/widgets/framework.dart:1133:30)

2 _MobileScannerState._startScanner. (package:mobile_scanner/src/mobile_scanner.dart:137:9)

(elided 24 frames from dart:async and package:stack_trace) I don't understand why. Why its giving me an exception if im 'overwriting' that method?
navaronbracke commented 9 months ago

@Andre-Coelhoo You aren't mocking the right channel.

You should update the name of the channel to match, so

void mockMobileScanner() {
  const channel = MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
  handler(MethodCall methodCall) async {
    if (methodCall.method == '_startScanner') {
      return null;
    }

    return null;
  }

  TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
      .setMockMethodCallHandler(channel, handler);
}

See https://github.com/juliansteenbakker/mobile_scanner/blob/master/lib/src/mobile_scanner_controller.dart#L66-L69

@juliansteenbakker We should really be marking the Method/Event Channels as @visibleForTesting and making them public. Then people don't have to rely on guessing the names of the channels for their tests.

Andre-Coelhoo commented 9 months ago

Sorry to keep you busy but I get this exception. it gives me the impression that it can't find the method to override

The following MissingPluginException was thrown while de-activating platform stream on channel dev.steenbakker.mobile_scanner/scanner/event: MissingPluginException(No implementation found for method cancel on channel dev.steenbakker.mobile_scanner/scanner/event)

When the exception was thrown, this was the stack:

0 MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:313:7)

(elided one frame from package:stack_trace) ═══════════════════════════════════════════════════
navaronbracke commented 9 months ago

@Andre-Coelhoo For event channels there is https://api.flutter.dev/flutter/flutter_test/TestDefaultBinaryMessenger/setMockStreamHandler.html

ibelz commented 8 months ago

@Andre-Coelhoo You aren't mocking the right channel.

You should update the name of the channel to match, so

void mockMobileScanner() {
 const channel = MethodChannel('dev.steenbakker.mobile_scanner/scanner/method');
 handler(MethodCall methodCall) async {
   if (methodCall.method == '_startScanner') {
     return null;
   }

   return null;
 }

 TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger
     .setMockMethodCallHandler(channel, handler);
}

See https://github.com/juliansteenbakker/mobile_scanner/blob/master/lib/src/mobile_scanner_controller.dart#L66-L69

@juliansteenbakker We should really be marking the Method/Event Channels as @visibleForTesting and making them public. Then people don't have to rely on guessing the names of the channels for their tests.

Thanks for the sample. The MissingPluginException is gone.

But if I use this mock I get: "mobile_scanner: MobileScannerException: permissionDenied"

Any ideas for this?

navaronbracke commented 8 months ago

Mock the request method as well? https://github.com/juliansteenbakker/mobile_scanner/blob/70f71a7956d7bca8d03e77c5ad3338b38e39cb7f/lib/src/mobile_scanner_controller.dart#L165-L187

RahmiTufanoglu commented 7 months ago

@navaronbracke Can we get some simple test examples for the MethodChannel and EventChannel implementations?

navaronbracke commented 7 months ago

@RahmiTufanoglu Sure!

So you have to use https://api.flutter.dev/flutter/flutter_test/TestDefaultBinaryMessenger/setMockMethodCallHandler.html or https://api.flutter.dev/flutter/flutter_test/TestDefaultBinaryMessenger/setMockStreamHandler.html

depending on whether it is a MethodChannel or an EventChannel.

For example for a MethodChannel:

// 'channel' is the MethodChannel to mock
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
  channel,
  (MethodCall methodCall) {
    // ...
  }
);

or an EventChannel:

// 'channel' is the EventChannel to mock
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler(
  channel,
  MockStreamHandler.inline(
    onListen: (Object? arguments, MockStreamHandlerEventSink events) {
     // ...
    },
    onCancel: (Object? arguments) {
      // ...
    },
  ),
);

The code sample assumes that the TestDefaultBinaryMessengerBinding.instance is non-null (which is not the case in older Flutter versions)

Note: Since mobile_scanner does not yet provide the channels as @visibleForTesting you have to define them manually for now. (The names in the channels should match what is used internally) I plan on fixing this soon, though.

RahmiTufanoglu commented 7 months ago

@navaronbracke Nice, I did something similar.

var canceled = false;

const channel = EventChannel('dev.steenbakker.mobile_scanner/scanner/event');

TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockStreamHandler(
  channel,
  MockStreamHandler.inline(
    onListen: (arguments, events) {
      events
        ..success('42')
        ..endOfStream();
    },
    onCancel: (arguments) {
      canceled = true;
    },
  ),
);

final events = await channel.receiveBroadcastStream().toList();
expect(events, orderedEquals(['42']));
navaronbracke commented 7 months ago

You do not give it an argument. channel.receiveBroadcastStream() returns the event stream from the underlying event sink. So you get all the events from that event stream anyway. IIRC these events are the scanned code events.

RahmiTufanoglu commented 7 months ago

@navaronbracke

I have the following scenario:

When a widget/page is opened, an onBarcodeDetected method is called. As soon as the device is able to scan something, it navigates to the next page. How can I test the onDetect method of the MobileScanner to see if it returns a valid value?

navaronbracke commented 7 months ago

The onDetect returns a BarcodeCapture intance.

You could test if that object contains a valid barcode in the barcodes property.

RahmiTufanoglu commented 7 months ago

@navaronbracke

I hope I'm not stealing too much of your time. Do you have a simple example?

navaronbracke commented 7 months ago

I don't have a working example up-front, but I can give some pointers.

  1. Set up a mock method call handler for the required methods for the MobileScannerController. I.e. request, state and the event channel as well. Why do we mock this? Because we cannot use a real device to point at a real code during the test :)
  2. In your integration test, pump a MobileScanner widget, passing in the onDetect function, using the WidgetTester
  3. Make sure that the MobileScannerController is started (you might need to start it manually before pumping the widget)
  4. Now that the widget is set up, send a mock barcode event, using the MockStreamHandler. (this is because when the camera detects a barcode, it sends a barcode event through the event sink)
  5. In onDetect, capture the 'barcode'
  6. Match the barcode against your expected result, using expect()

Bear in mind, I have not tested this flow. The key point is that the camera cannot really be used to scan the barcode during a test (there is nobody holding the barcode in front of the device, so to speak) . Therefor you provide mocks for that part of the test

navaronbracke commented 7 months ago

I do plan on writing tests for mobile_scanner itself, that verify this behavior, but that is on my TODO list down the line.

RahmiTufanoglu commented 7 months ago

I try to create an example. Maybe I can contribute