Skip to content

テスト

Flutterでのテストは、アプリケーションの品質を保証するために重要です。以下に、テストの方法を紹介します。

ユニットテストは、個々の関数やメソッドが正しく動作することを確認するためのテストです。testパッケージを使用して、ユニットテストを実行します。

test('Counter increments smoke test', () {
final counter = Counter();
counter.increment();
expect(counter.value, 1);
});
  • test: テストケースを定義するための関数です。
  • expect: 実際の値と期待される値を比較します。

ウィジェットテストは、UIの動作を確認するためのテストです。flutter_testパッケージを使用して、ウィジェットテストを実行します。

testWidgets('MyWidget has a title and message', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
final titleFinder = find.text('T');
final messageFinder = find.text('M');
expect(titleFinder, findsOneWidget);
expect(messageFinder, findsOneWidget);
});
  • testWidgets: ウィジェットテストを定義するための関数です。
  • pumpWidget: ウィジェットをテスト環境にロードします。
  • インテグレーションテスト: アプリ全体の動作を確認するためのテストです。integration_testパッケージを使用して、インテグレーションテストを実行します。
  • モックテスト: 外部依存をモック化して、特定の機能をテストします。

テストを行う際のベストプラクティスや、よくある問題の解決策を以下に示します。

  • テストの自動化: CI/CDパイプラインにテストを組み込み、自動化することで、継続的な品質保証を実現します。
  • テストのカバレッジ: テストカバレッジを高めるために、重要な機能やエッジケースを網羅するテストケースを作成します。
  • テストの分離: 各テストケースは独立して実行できるように設計し、他のテストに依存しないようにします。
  • テストの失敗: テストが失敗した場合、エラーメッセージをよく読み、原因を特定して修正します。flutter test --verboseを使用して、詳細なログを確認します。

  • 非同期処理のテスト: 非同期処理をテストする際は、async/awaitを使用して、テストが完了するまで待機します。

test('Async test example', () async {
final result = await fetchData();
expect(result, isNotNull);
});
  • モックの使用: 外部依存をモック化して、特定の機能をテストします。mockitoパッケージを使用して、モックを作成します。
dev_dependencies:
mockito: ^5.4.0
build_runner: ^2.4.0
user_repository.dart
abstract class UserRepository {
Future<User> getUserById(String id);
}
// user_repository_test.mocks.dart (自動生成)
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'user_repository.dart';
@GenerateMocks([UserRepository])
void main() {}
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
import 'user_repository_test.mocks.dart';
@GenerateMocks([UserRepository])
void main() {
late MockUserRepository mockRepository;
late UserService userService;
setUp(() {
mockRepository = MockUserRepository();
userService = UserService(mockRepository);
});
test('getUserById returns user when repository succeeds', () async {
// Arrange
final expectedUser = User(id: '1', name: 'Test User');
when(mockRepository.getUserById('1'))
.thenAnswer((_) async => expectedUser);
// Act
final result = await userService.getUserById('1');
// Assert
expect(result, equals(expectedUser));
verify(mockRepository.getUserById('1')).called(1);
});
test('getUserById throws exception when repository fails', () async {
// Arrange
when(mockRepository.getUserById('1'))
.thenThrow(Exception('User not found'));
// Act & Assert
expect(
() => userService.getUserById('1'),
throwsException,
);
});
}
Terminal window
flutter pub run build_runner build

4. インテグレーションテストの実装

Section titled “4. インテグレーションテストの実装”

integration_testディレクトリの作成

Section titled “integration_testディレクトリの作成”

プロジェクトルートにintegration_testディレクトリを作成します。

インテグレーションテストの例

Section titled “インテグレーションテストの例”
integration_test/app_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('end-to-end test', () {
testWidgets('complete user flow', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
// ログイン画面の確認
expect(find.text('ログイン'), findsOneWidget);
// ログイン
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), 'password');
await tester.tap(find.byKey(Key('loginButton')));
await tester.pumpAndSettle();
// ホーム画面の確認
expect(find.text('ホーム'), findsOneWidget);
// アイテムの追加
await tester.tap(find.byKey(Key('addButton')));
await tester.pumpAndSettle();
expect(find.text('新しいアイテム'), findsOneWidget);
});
});
}

インテグレーションテストの実行

Section titled “インテグレーションテストの実行”
Terminal window
# デバイスで実行
flutter test integration_test/app_test.dart
# 特定のデバイスで実行
flutter test integration_test/app_test.dart -d <device_id>
/\
/ \ E2E Tests (少数)
/____\
/ \ Integration Tests (中程度)
/________\
/ \ Unit Tests (多数)
/____________\
  1. ユニットテスト: 個々の関数やクラスをテスト
  2. ウィジェットテスト: UIコンポーネントをテスト
  3. インテグレーションテスト: 複数のコンポーネントの連携をテスト
  4. E2Eテスト: アプリ全体の動作をテスト
Terminal window
# カバレッジレポートの生成
flutter test --coverage
# カバレッジの確認(lcovが必要)
genhtml coverage/lcov.info -o coverage/html
.github/workflows/flutter_test.yml
name: Flutter Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Install dependencies
run: flutter pub get
- name: Run tests
run: flutter test
- name: Generate coverage
run: flutter test --coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
.gitlab-ci.yml
stages:
- test
flutter_test:
stage: test
image: cirrusci/flutter:latest
script:
- flutter pub get
- flutter test
- flutter test --coverage
coverage: '/lines\.*: \d+\.\d+%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/lcov.info

7. テストのベストプラクティス

Section titled “7. テストのベストプラクティス”
test('example test', () {
// Arrange: テストの準備
final calculator = Calculator();
// Act: テスト対象の実行
final result = calculator.add(2, 3);
// Assert: 結果の検証
expect(result, equals(5));
});

各テストは独立して実行できるようにします。

class TestHelper {
static UserRepository createMockRepository() {
return MockUserRepository();
}
static User createTestUser() {
return User(id: '1', name: 'Test User');
}
}
void main() {
test('test 1', () {
final repository = TestHelper.createMockRepository();
// テスト実行
});
test('test 2', () {
final repository = TestHelper.createMockRepository();
// テスト実行(test 1に依存しない)
});
}
class TestData {
static User validUser() => User(
id: '1',
name: 'Test User',
email: 'test@example.com',
);
static User invalidUser() => User(
id: '',
name: '',
email: '',
);
}

問題1: 非同期処理のテストが失敗する

Section titled “問題1: 非同期処理のテストが失敗する”

解決策: pumpAndSettlepumpを使用して、非同期処理の完了を待ちます。

testWidgets('async test', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
await tester.pumpAndSettle(); // すべてのアニメーションが完了するまで待つ
expect(find.text('Loaded'), findsOneWidget);
});

問題2: モックが正しく動作しない

Section titled “問題2: モックが正しく動作しない”

解決策: build_runnerでモックを再生成します。

Terminal window
flutter pub run build_runner build --delete-conflicting-outputs

解決策:

  • 不要なpumpAndSettleを避ける
  • モックを使用して外部依存を排除
  • テストを並列実行
Terminal window
flutter test --concurrency=4

これで、Flutterでのテスト実装方法を理解できるようになりました。