12

I am creating a flutter app that uses the native camera to take a photo, using the official flutter camera package (https://pub.dev/packages/camera). The app opens up a modal that loads a CameraPreview based on the the result of the availableCameras function from the package and a FloatingActionButton which takes a photo when pressed. While creating a widget test for this modal, I can not figure out how to stub the availableCameras function to return what I want during tests.

I tried using the Mockito testing package, but this only supports mocking classes. Since this function does not belong to a class, I cannot mock it.

The availableCameras function returns a list of cameras that the device has. I want to be able to control what comes back from this function, so that I may test how my widget reacts to different cameras. What is the proper way to have this function return what I want during a widget test?

Bakkingamu
  • 141
  • 1
  • 4

3 Answers3

17

Mockito can mock functions too. In dart, functions are classes with a call method.

You can, therefore, use Mockito as usual, with an abstract call method:

class MockFunction extends Mock {
  int call(String param);
}

This example represents a int Function(String param).

Which means you can then do:

final int Function(String) myFn = MockFunction();
when(myFn('hello world')).thenReturn(42);

expect(myFn('hello world'), equals(42));
Rémi Rousselet
  • 256,336
  • 79
  • 519
  • 432
  • Very interesting, but wouldn't this work only if the function was injected (as an argument, for instance)? Can you please provide an example of mocking a top-level function? – Hugo Passos Aug 21 '19 at 00:04
  • @HugoPassos yes, you need a mean to replace the function with another. There are tons of different ways to do so though. For example, the function could be a mutable global variable. – Rémi Rousselet Aug 21 '19 at 00:12
  • There are entire books dedicated to these patterns, so I can't reasonably give a definitive solution here. – Rémi Rousselet Aug 21 '19 at 00:12
  • Yeah, but we're talking about a public library. I can't think in any way to replace the function in this situation. – Hugo Passos Aug 21 '19 at 00:28
  • You don't have to use the function directly. A global `var getCameras = availableCameras` should do the trick. Then your test can replace it with a different value. – Rémi Rousselet Aug 21 '19 at 00:57
  • You're right. I initially thought `availableCameras` was called by the library, not by the user. – Hugo Passos Aug 21 '19 at 01:19
  • @HugoPassos may i check you resolved this issue? i called availableCameras() inside my real bloc class but have no idea how to mock it. – kabayaba Aug 20 '20 at 09:07
2

In this very specific situation, you can mock the method channel call handler.

const cameraMethodChannel = MethodChannel('plugins.flutter.io/camera');

setUpAll(() {
  cameraMethodChannel.setMockMethodCallHandler(cameraCallHandler);
});

tearDownAll(() {
  cameraMethodChannel.setMockMethodCallHandler(null);
});

Future<dynamic> cameraCallHandler(MethodCall methodCall) async {
  if (methodCall.method == 'availableCameras') return yourListOfCameras;
}
Hugo Passos
  • 7,719
  • 2
  • 35
  • 52
0

Remi's answer here is correct. Here is a more complete recent example that returns a future. This article explains how you can build the widget test around your existing code. this uses mocktail.

import 'package:gistfile/main.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:flutter/material.dart';

class LaunchMock extends Mock {
  Future<bool> call(
    Uri url, {
    LaunchMode? mode,
    WebViewConfiguration? webViewConfiguration,
    String? webOnlyWindowName,
  });
}

void main() {
  testWidgets('Test Url Launch', (tester) async {
    //These allow default values
    registerFallbackValue(LaunchMode.platformDefault);
    registerFallbackValue(const WebViewConfiguration());

    //Create the mock
    final mock = LaunchMock();
    when(() => mock(
          flutterDevUri,
          mode: any(named: 'mode'),
          webViewConfiguration: any(named: 'webViewConfiguration'),
          webOnlyWindowName: any(named: 'webOnlyWindowName'),
        )).thenAnswer((_) async => true);

    final builder = compose()
      //Replace the launch function with a mock
      ..addSingletonService<LaunchUrl>(mock);

    await tester.pumpWidget(
      builder.toContainer()<MyApp>(),
    );

    //Tap the icon
    await tester.tap(
      find.byIcon(Icons.favorite),
    );

    await tester.pumpAndSettle();

    verify(() => mock(flutterDevUri)).called(1);
  });
}
Christian Findlay
  • 6,770
  • 5
  • 51
  • 103