目次
概要
Flutter
で写真撮影などのカメラ機能を使うためにはFlutter
が提供するCamera
プラグインを使う必要があります。
- Cameraプラグイン: https://pub.dev/packages/camera
以前のブログポストではCamera
プラグインを使ってFlutter
で写真撮影機能を実装する方法について説明しました。
今回のブログポストではCamera
プラグインを使って実装した写真撮影機能に関するテストコードを作成する方法について説明します。ここで紹介するソースコードはGitHub
で確認できます。
Cameraプラグインを使って写真撮影機能
Camera
プラグインを使った写真撮影機能は以前のブログポストを参考してください。
今回のブログポストでは当該ブログポストのコード活用して進め予定です。
Mockitoのインストール
Camera
プラグインを使って実装した写真撮影機能に関するテストコードを作成するためにはMockito
を使ってCamera
プラグインのMock
を作成する必要があります。
- Mockito: https://pub.dev/packages/mockito
Mockito
を使うために次のコマンドを実行して、Mockito
をインストールします。
flutter pub add mockito
Mockの生成
Camera
プラグインに関するMock
を生成するため、test/camera_mock.dart
ファイルを生成して次のように作成します。
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;
}
この内容はCamera
プラグインのGitHubレポジトリ(GitHub repository)を参考して必要な部分だけをまとめて作成しました。
- 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注入
Camera
プラグインを使って作った写真撮影機能に関するテストコードを作成するため、既に実装したコードでCameraController
を外部から渡してもらえるように修正する必要があります。
CameraController
を外部から注入(inject)できるようにするため、lib/camera_screen.dart
ファイルを開いて下記のように修正します。
...
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) {
...
}
});
}
...
}
外部からCameraController
を設定できるように修正して、CameraController
が外部から注入された場合、そのCameraController
を使うように修正しました。
テストコード作成
次はCamera
プラグインを使って作った写真撮影機能に関するテストコードを作成してみます。test/camera_screen_test.dart
ファイルを作って次のように修正します。
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();
});
...
}
Camera
プラグインを使った写真撮影のテストコードを作成するためにはCameraPlatform
のinstance
に先ほど作ったMock
を設定する必要があります。
使えるカメラがない場合
Camera
プラグインは利用できるカメラがある場合、写真撮影機能が使えます。
...
availableCameras().then((cameras) {
if (cameras.isNotEmpty) {
_cameraController = CameraController(
cameras.first,
ResolutionPreset.medium,
);
_cameraController!.initialize().then((_) {
setState(() {
_isCameraReady = true;
});
});
}
});
...
従って利用できるカメラがない場合をテストする必要があります。利用できるカメラがない場合のテストコードは次のように作成します。
...
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;
});
...
使えるカメラがないように作るためmockAvailableCameras
に空のリストを設定する必要があります。しかし、一度空のリストを設定すると他のテストコードでも使えるカメラがないようになるため、テストが終わったら元のmockAvailableCameras
に戻す必要があります。
使えるカメラがある場合
利用可能なカメラがある場合、CameraController
が初期化sあれて、CameraPreview
が画面い表示されるようになります。
...
Expanded(
flex: 1,
child: _cameraController != null && _isCameraReady
? CameraPreview(_cameraController!)
: Container(
color: Colors.grey,
),
),
...
これを確認するためのテストコードは次のように作成します。
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);
});
利用可能なCameraController
を事前に作って注入した後、CameraPreview
が画面にうまく表示されるか確認しました。
写真撮影テスト
最後にCamera
プラグインを使って写真撮影がうまくできるか確認するテストコードを作成してみます。Camera
プラグインを使って問題なく写真が撮影できたら写真撮影結果のパスをPhotoPreview
画面に渡して撮影した写真を確認できるように構成しました。
...
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'),
),
),
...
これをテストするためのテストコードは次のように作成します。
...
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)));
});
...
先ほど作ったMockCameraController
を使ってテスtコードで写真撮影ができるようにしました。写真撮影が終わったらPhotoPreview
画面に移動して撮影された写真を画面に表示するので、PhotoPreview
画面に移動したか、そして撮影された写真が画面にうまく表示されたかを確認しました。
完了
これでFlutter
でCamera
プラグインを使って実装した写真撮影機能に関するテストコードを作成する方法についてみてみました。もっと詳しいテストコードや他の機能に関するテストコードについては公式のGitHub
レポジトリを確認してください。
私のブログが役に立ちましたか?下にコメントを残してください。それは私にとって大きな大きな力になります!
アプリ広報
Deku
が開発したアプリを使ってみてください。Deku
が開発したアプリはFlutterで開発されています。興味がある方はアプリをダウンロードしてアプリを使ってくれると本当に助かります。