Contents
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
.
- Camera plugin: https://pub.dev/packages/camera
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
.
- Mockito: https://pub.dev/packages/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.
- https://github.com/flutter/packages/blob/main/packages/camera/camera/test/camera_test.dart#L17-L53
- https://github.com/flutter/packages/blob/main/packages/camera/camera/test/camera_test.dart#L1371-L1537
- https://github.com/flutter/packages/blob/main/packages/camera/camera/test/camera_preview_test.dart#L12-L125
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.
- Official GitHub repository: https://github.com/flutter/packages/tree/main/packages/camera/camera/test
Was my blog helpful? Please leave a comment at the bottom. it will be a great help to me!
App promotion
Deku
.Deku
created the applications with Flutter.If you have interested, please try to download them for free.