[Flutter] Test code for Camera plugin

[Flutter] Test code for Camera plugin

2023-07-15 hit count image

Let's see how to write the test code for the photo shooting feature implemented by the Camera plugin in Flutter.

Outline

In order to use the camera function such as taking a picture in Flutter, you need to use the Camera plugin provided by Flutter.

In the previous blog post, I introduced how to implement the photo shooting function in Flutter using the Camera plugin.

In this blog post, I will introduce how to write a test code for the photo shooting function implemented using the Camera plugin. The source code introduced here can be found on GitHub.

Implement photo shooting function using Camera plugin

For the function of taking pictures using the Camera plugin, please refer to the previous blog post.

In this blog post, I will use the code of the previous blog post to implement the test code.

Install Mockito

In order to write a test code for the photo shooting function implemented using the Camera plugin, it is necessary to write a Mock of the Camera plugin using Mockito.

To use Mockito, run the following command to install Mockito.

flutter pub add mockito

Create Mock

In order to create a Mock for the Camera plugin, create the test/camera_mock.dart file and modify it as follows.

import 'dart:math';

import 'package:camera/camera.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';

List<CameraDescription> mockAvailableCameras = <CameraDescription>[
  const CameraDescription(
      name: 'camBack',
      lensDirection: CameraLensDirection.back,
      sensorOrientation: 90),
  const CameraDescription(
      name: 'camFront',
      lensDirection: CameraLensDirection.front,
      sensorOrientation: 180),
];

bool mockPlatformException = false;

int get mockInitializeCamera => 13;

CameraInitializedEvent get mockOnCameraInitializedEvent =>
    const CameraInitializedEvent(
      13,
      75,
      75,
      ExposureMode.auto,
      true,
      FocusMode.auto,
      true,
    );

CameraClosingEvent get mockOnCameraClosingEvent => const CameraClosingEvent(13);

CameraErrorEvent get mockOnCameraErrorEvent =>
    const CameraErrorEvent(13, 'closing');

DeviceOrientationChangedEvent get mockOnDeviceOrientationChangedEvent =>
    const DeviceOrientationChangedEvent(DeviceOrientation.portraitUp);

XFile mockTakePicture = XFile('foo/bar.png');

XFile mockVideoRecordingXFile = XFile('foo/bar.mpeg');

class MockCameraPlatform extends Mock
    with MockPlatformInterfaceMixin
    implements CameraPlatform {
  @override
  Future<void> initializeCamera(
    int? cameraId, {
    ImageFormatGroup? imageFormatGroup = ImageFormatGroup.unknown,
  }) async =>
      super.noSuchMethod(Invocation.method(
        #initializeCamera,
        <Object?>[cameraId],
        <Symbol, dynamic>{
          #imageFormatGroup: imageFormatGroup,
        },
      ));

  @override
  Future<void> dispose(int? cameraId) async {
    return super.noSuchMethod(Invocation.method(#dispose, <Object?>[cameraId]));
  }

  @override
  Future<List<CameraDescription>> availableCameras() =>
      Future<List<CameraDescription>>.value(mockAvailableCameras);

  @override
  Future<int> createCamera(
    CameraDescription description,
    ResolutionPreset? resolutionPreset, {
    bool enableAudio = false,
  }) =>
      mockPlatformException
          ? throw PlatformException(code: 'foo', message: 'bar')
          : Future<int>.value(mockInitializeCamera);

  @override
  Stream<CameraInitializedEvent> onCameraInitialized(int cameraId) =>
      Stream<CameraInitializedEvent>.value(mockOnCameraInitializedEvent);

  @override
  Stream<CameraClosingEvent> onCameraClosing(int cameraId) =>
      Stream<CameraClosingEvent>.value(mockOnCameraClosingEvent);

  @override
  Stream<CameraErrorEvent> onCameraError(int cameraId) =>
      Stream<CameraErrorEvent>.value(mockOnCameraErrorEvent);

  @override
  Stream<DeviceOrientationChangedEvent> onDeviceOrientationChanged() =>
      Stream<DeviceOrientationChangedEvent>.value(
          mockOnDeviceOrientationChangedEvent);

  @override
  Future<XFile> takePicture(int cameraId) => mockPlatformException
      ? throw PlatformException(code: 'foo', message: 'bar')
      : Future<XFile>.value(mockTakePicture);

  @override
  Future<void> prepareForVideoRecording() async =>
      super.noSuchMethod(Invocation.method(#prepareForVideoRecording, null));

  @override
  Future<XFile> startVideoRecording(int cameraId,
          {Duration? maxVideoDuration}) =>
      Future<XFile>.value(mockVideoRecordingXFile);

  @override
  Future<void> startVideoCapturing(VideoCaptureOptions options) {
    return startVideoRecording(options.cameraId,
        maxVideoDuration: options.maxDuration);
  }

  @override
  Future<void> lockCaptureOrientation(
          int? cameraId, DeviceOrientation? orientation) async =>
      super.noSuchMethod(Invocation.method(
          #lockCaptureOrientation, <Object?>[cameraId, orientation]));

  @override
  Future<void> unlockCaptureOrientation(int? cameraId) async =>
      super.noSuchMethod(
          Invocation.method(#unlockCaptureOrientation, <Object?>[cameraId]));

  @override
  Future<void> pausePreview(int? cameraId) async =>
      super.noSuchMethod(Invocation.method(#pausePreview, <Object?>[cameraId]));

  @override
  Future<void> resumePreview(int? cameraId) async => super
      .noSuchMethod(Invocation.method(#resumePreview, <Object?>[cameraId]));

  @override
  Future<double> getMaxZoomLevel(int? cameraId) async => super.noSuchMethod(
        Invocation.method(#getMaxZoomLevel, <Object?>[cameraId]),
        returnValue: Future<double>.value(1.0),
      ) as Future<double>;

  @override
  Future<double> getMinZoomLevel(int? cameraId) async => super.noSuchMethod(
        Invocation.method(#getMinZoomLevel, <Object?>[cameraId]),
        returnValue: Future<double>.value(0.0),
      ) as Future<double>;

  @override
  Future<void> setZoomLevel(int? cameraId, double? zoom) async =>
      super.noSuchMethod(
          Invocation.method(#setZoomLevel, <Object?>[cameraId, zoom]));

  @override
  Future<void> setFlashMode(int? cameraId, FlashMode? mode) async =>
      super.noSuchMethod(
          Invocation.method(#setFlashMode, <Object?>[cameraId, mode]));

  @override
  Future<void> setExposureMode(int? cameraId, ExposureMode? mode) async =>
      super.noSuchMethod(
          Invocation.method(#setExposureMode, <Object?>[cameraId, mode]));

  @override
  Future<void> setExposurePoint(int? cameraId, Point<double>? point) async =>
      super.noSuchMethod(
          Invocation.method(#setExposurePoint, <Object?>[cameraId, point]));

  @override
  Future<double> getMinExposureOffset(int? cameraId) async =>
      super.noSuchMethod(
        Invocation.method(#getMinExposureOffset, <Object?>[cameraId]),
        returnValue: Future<double>.value(0.0),
      ) as Future<double>;

  @override
  Future<double> getMaxExposureOffset(int? cameraId) async =>
      super.noSuchMethod(
        Invocation.method(#getMaxExposureOffset, <Object?>[cameraId]),
        returnValue: Future<double>.value(1.0),
      ) as Future<double>;

  @override
  Future<double> getExposureOffsetStepSize(int? cameraId) async =>
      super.noSuchMethod(
        Invocation.method(#getExposureOffsetStepSize, <Object?>[cameraId]),
        returnValue: Future<double>.value(1.0),
      ) as Future<double>;

  @override
  Future<double> setExposureOffset(int? cameraId, double? offset) async =>
      super.noSuchMethod(
        Invocation.method(#setExposureOffset, <Object?>[cameraId, offset]),
        returnValue: Future<double>.value(1.0),
      ) as Future<double>;
}

class MockCameraController extends ValueNotifier<CameraValue>
    implements CameraController {
  MockCameraController()
      : super(const CameraValue.uninitialized(fakeDescription));

  static const CameraDescription fakeDescription = CameraDescription(
      name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0);

  @override
  Future<void> dispose() async {
    super.dispose();
  }

  @override
  Widget buildPreview() {
    return const Texture(textureId: CameraController.kUninitializedCameraId);
  }

  @override
  int get cameraId => CameraController.kUninitializedCameraId;

  @override
  void debugCheckIsDisposed() {}

  @override
  bool get enableAudio => false;

  @override
  Future<double> getExposureOffsetStepSize() async => 1.0;

  @override
  Future<double> getMaxExposureOffset() async => 1.0;

  @override
  Future<double> getMaxZoomLevel() async => 1.0;

  @override
  Future<double> getMinExposureOffset() async => 1.0;

  @override
  Future<double> getMinZoomLevel() async => 1.0;

  @override
  ImageFormatGroup? get imageFormatGroup => null;

  @override
  Future<void> initialize() async {}

  @override
  Future<void> lockCaptureOrientation([DeviceOrientation? orientation]) async {}

  @override
  Future<void> pauseVideoRecording() async {}

  @override
  Future<void> prepareForVideoRecording() async {}

  @override
  ResolutionPreset get resolutionPreset => ResolutionPreset.low;

  @override
  Future<void> resumeVideoRecording() async {}

  @override
  Future<void> setExposureMode(ExposureMode mode) async {}

  @override
  Future<double> setExposureOffset(double offset) async => offset;

  @override
  Future<void> setExposurePoint(Offset? point) async {}

  @override
  Future<void> setFlashMode(FlashMode mode) async {}

  @override
  Future<void> setFocusMode(FocusMode mode) async {}

  @override
  Future<void> setFocusPoint(Offset? point) async {}

  @override
  Future<void> setZoomLevel(double zoom) async {}

  @override
  Future<void> startImageStream(onLatestImageAvailable onAvailable) async {}

  @override
  Future<void> startVideoRecording(
      {onLatestImageAvailable? onAvailable}) async {}

  @override
  Future<void> stopImageStream() async {}

  @override
  Future<XFile> stopVideoRecording() async => XFile('');

  @override
  Future<XFile> takePicture() async => Future<XFile>.value(mockTakePicture);

  @override
  Future<void> unlockCaptureOrientation() async {}

  @override
  Future<void> pausePreview() async {}

  @override
  Future<void> resumePreview() async {}

  @override
  Future<void> setDescription(CameraDescription description) async {}

  @override
  CameraDescription get description => value.description;
}

The test code was made by collecting only the necessary parts by referring to the GitHub repository of the Camera plugin.

CameraController injection

In order to write a test code for the photo shooting function implemented using the Camera plugin, it is necessary to modify the code implemented in the previous blog post to receive the CameraController from the outside.

To make the CameraController injectable from the outside, open the lib/camera_screen.dart file and modify it as follows.

...
class CameraScreen extends StatefulWidget {
  final CameraController? cameraController;

  const CameraScreen({
    this.cameraController,
    super.key,
  });
  ...
}

class _CameraScreenState extends State<CameraScreen> {
  ...
  @override
  void initState() {
    super.initState();
    _cameraController = widget.cameraController;
    if (_cameraController != null) {
      _isCameraReady = true;
      return;
    }

    availableCameras().then((cameras) {
      if (cameras.isNotEmpty) {
        ...
      }
    });
  }
  ...
}

Now, when the CameraController is injected from the outside, the injected CameraController is used.

Write test code

Now, let’s write a test code for the photo shooting function implemented using the Camera plugin. Create the test/camera_screen_test.dart file and modify it as follows.

import 'dart:io';

import 'package:camera/camera.dart';
import 'package:camera_example/camera_screen.dart';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import './camera_mock.dart';

void main() {
  setUp(() {
    CameraPlatform.instance = MockCameraPlatform();
  });
  ...
}

In order to write a test code for the photo shooting function implemented using the Camera plugin, it is necessary to set the Mock created earlier to the instance of CameraPlatform.

When there is no available camera

The Camera plugin can use the photo shooting function if there is an available camera.

...
availableCameras().then((cameras) {
  if (cameras.isNotEmpty) {
    _cameraController = CameraController(
      cameras.first,
      ResolutionPreset.medium,
    );

    _cameraController!.initialize().then((_) {
      setState(() {
        _isCameraReady = true;
      });
    });
  }
});
...

Therefore, it is necessary to test the case where there is no available camera. The test code for the case that there is no available camera is as follows.

...
testWidgets('Rendered well when camera is not available', (
    WidgetTester tester,
  ) async {
  final originalAvailableCameras = mockAvailableCameras;
  mockAvailableCameras = [];

  await tester.pumpWidget(const MaterialApp(home: CameraScreen()));
  await tester.pumpAndSettle();

  final scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
  final cameraPreview =
      (((scaffold.body as SafeArea).child as Column).children[0] as Expanded)
          .child;
  expect(cameraPreview.runtimeType, Container);

  mockAvailableCameras = originalAvailableCameras;
});
...

To make the camear unavailable, you need to set the empty list to mokAvailableCameras. However, once you set it to the empty list, it will be recognized as there is no available camera in other test codes, so you need to set it back to the original mockAvailableCameras at the end of the test.

When there is available camera

If there is an available camera, the CameraController is initialized and the CameraPreview is displayed on the screen.

...
Expanded(
  flex: 1,
  child: _cameraController != null && _isCameraReady
      ? CameraPreview(_cameraController!)
      : Container(
          color: Colors.grey,
        ),
),
...

The test code for this is as follows.

testWidgets('Rendered well when camera is available', (
    WidgetTester tester,
  ) async {
  final cameraController = CameraController(
    const CameraDescription(
      name: 'cam',
      lensDirection: CameraLensDirection.back,
      sensorOrientation: 90,
    ),
    ResolutionPreset.max,
  );

  await tester.pumpWidget(
    MaterialApp(
      home: CameraScreen(
        cameraController: cameraController,
      ),
    ),
  );
  await tester.pumpAndSettle();

  final scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
  final cameraPreview =
      (((scaffold.body as SafeArea).child as Column).children[0] as Expanded)
          .child;
  expect(cameraPreview.runtimeType, CameraPreview);
});

Make the CameraController that can be used in the test code and inject it in advance, and then check if the CameraPreview is displayed well.

Test photo shooting

Lastly, let’s write a test code to check if the photo shooting feature works well using the Camera plugin. If the photo shooting is successful using the Camera plugin, the path of the photo shooting result is passed to the PhotoPreview screen to check the photo shooting result.

...
void _onTakePicture(BuildContext context) {
  _cameraController!.takePicture().then((image) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => PhotoPreview(
          imagePath: image.path,
        ),
      ),
    );
  });
}
...
Padding(
  padding: const EdgeInsets.all(16),
  child: ElevatedButton(
    onPressed: _cameraController != null
        ? () => _onTakePicture(context)
        : null,
    child: const Text('Take a photo'),
  ),
),
...

The test code for this is as follows.

...
testWidgets('Take a picture', (
    WidgetTester tester,
  ) async {
  final mockCameraController = MockCameraController();
  mockCameraController.value = mockCameraController.value.copyWith(
    isInitialized: true,
    previewSize: const Size(480, 640),
  );

  await tester.pumpWidget(
    MaterialApp(
      home: CameraScreen(
        cameraController: mockCameraController,
      ),
    ),
  );
  await tester.pumpAndSettle();

  var scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
  expect(((scaffold.appBar as AppBar).title as Text).data, 'Take a photo');

  await tester.tap(find.byType(ElevatedButton));
  await tester.pumpAndSettle();

  scaffold = tester.widget<Scaffold>(find.byType(Scaffold));
  expect(((scaffold.appBar as AppBar).title as Text).data, 'Preview');
  final image = tester.widget<Image>(find.byType(Image));
  expect(image.image, FileImage(File(mockTakePicture.path)));
});
...

I make the photo shooting available by using MockCameraController created above. When the photo is taken, the PhotoPreview screen will be shown with the result of the photo shooting, so I checked the PhotoPreview screen is shown and the photo is displayed well.

Completed

Done! we’ve seen how to write the test code for the photo shooting function implemented using the Camera plugin in Flutter. For more detailed test code or test code for other features, please check the official GitHub repository.

Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!

App promotion

You can use the applications that are created by this blog writer Deku.
Deku created the applications with Flutter.

If you have interested, please try to download them for free.

Posts