初心者と学ぶFlutter第7回:テスト

初心者と学ぶFlutter

はじめに

前回はTodoアプリをローカルデータに保存するところまで実施しました。

初心者と学ぶFlutter第6回:ローカルデータの保存
shared_preferencesを使ってFlutterアプリのデータをローカルに保存し、アプリ再起動時にもTodoリストを保持する方法を詳しく解説します。

今回はテストについて学びます。

アプリを開発する際、テストは非常に重要です。テストはアプリの品質を保つため、Flutterには、テストをサポートする便利なツールが用意されています。

今回の記事では、Flutterの基本的な単体テスト(ユニットテスト)とウィジェットテストの実装方法を学びます。初心者でも理解できるよう、ステップごとに解説していきますので、ぜひ一緒に進めていきましょう。


テストの基本

テストは、アプリのバグを防ぎ、品質を保つために不可欠です。Flutterでは、簡単にテストを導入でき、ユニットテストやウィジェットテスト、統合テストがサポートされています。

単体テスト(ユニットテスト)

ユニットテストは、アプリの特定の関数やメソッドが正しく動作するかを確認するためのテストです。ロジック部分をテストすることで、予期せぬ動作を防ぎます。

テスト環境のセットアップ

プロジェクトを作成した時点で、セットアップは完了していますので、追加のセットアップは必要ありません。もし完了していない場合は、参考にしてください。

まず、testディレクトリを作成し、テストコードを配置します。

Bash
mkdir test

次に、Flutterプロジェクトのpubspec.yamlファイルで、testパッケージが含まれていることを確認します。通常、デフォルトで追加されています。

YAML
dev_dependencies:
  flutter_test:
    sdk: flutter

単体テストの実装例

下記は簡単な足し算を行う関数にです。

Dart
// lib/calculator.dart
class Calculator {
  int add(int a, int b) {
    return a + b;
  }
}

次に、この関数をテストします。

  • test(): 個別のテストケースを定義します。
  • expect(): 結果が期待通りかどうかを確認します。例えば、expect(actual, matcher)の形式で、actualmatcherと一致しているかチェックします。
Dart
// test/calculator_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:your_project/calculator.dart';

void main() {
  test('足し算のテスト', () {
    final calculator = Calculator();
    
    expect(calculator.add(2, 3), 5);
    expect(calculator.add(-1, 1), 0);
  });
}

flutter testコマンドを実行して、テストが正常に動作することを確認します。

Bash
flutter test

00:01 +0: 足し算のテスト                                                                                                                    00:01 +1: 足し算のテスト                                                                                                                    00:01 +1: All tests passed!   

ウィジェットテスト

ウィジェットテストは、Flutterのウィジェット(UI)をテストするための方法です。個々のウィジェットが正しく動作し、期待通りにレンダリングされるかを確認します。

ウィジェットテストの実装例

次に、Hello Worldを表示する簡単なウィジェットに対するテストを行ってみます。

Dart
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Hello World App')),
        body: Center(child: Text('Hello World')),
      ),
    );
  }
}

このウィジェットをテストするコードをtest/main_test.dartに記述します。

  • testWidgets(): ウィジェットテストを行うための関数。
  • WidgetTester: テスト中にウィジェットを操作するためのユーティリティ。tappumpを使って、ウィジェットを操作・再描画します。
  • find.text()find.byType(): ウィジェットを見つけるためのヘルパー関数。
Dart
// test/main_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_project/main.dart';

void main() {
  testWidgets('Hello Worldが表示されるかのテスト', (WidgetTester tester) async {
    // アプリをビルド
    await tester.pumpWidget(MyApp());

    // "Hello World"というテキストが表示されているかを確認
    expect(find.text('Hello World'), findsOneWidget);
  });
}

このテストは、ウィジェットが期待通りに描画され、Hello Worldというテキストが正しく表示されているかを確認します。テストを実行するには、再びflutter testコマンドを使います。

Bash
flutter test

00:01 +0: Hello Worldが表示されるかのテスト                                                                                                 00:01 +1: Hello Worldが表示されるかのテスト                                                                                                 00:01 +1: All tests passed! 

Todoアプリのテスト

前回まで作成したTodoアプリのテストを作ってみます。

今回作成したTodoアプリ内にある_TodoListScreenState は、アンダースコア(_)がついているためプライベートクラスとして定義されています。このため、クラスは同じファイル内からしかアクセスできません。そのため、テストファイルから直接アクセスできません。
そのためウィジェットテストでの間接的なテストを行います。
これにより、画面の操作(追加、削除、編集など)をシミュレートし、テストできます。

テスト

  • testWidgets: ウィジェットテストを行うための関数。これにより、TodoApp全体をビルドし、その中でユーザー操作(タップや入力)をシミュレートしてテストします。
  • tester.pumpWidget: TodoAppをウィジェットツリーに挿入して、テスト対象とします。
  • tester.tap: ボタンをタップする操作をシミュレートします。
  • tester.enterText: テキストフィールドにテキストを入力します。
  • tester.pumpAndSettle: すべてのウィジェットのアニメーションや再描画が完了するまで待ちます。
Dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:my_first_app/main.dart';

void main() {
  setUp(() async {
    SharedPreferences.setMockInitialValues({}); // SharedPreferencesをモックで初期化
  });

  testWidgets('Todo item is added correctly', (WidgetTester tester) async {
    await tester.pumpWidget(TodoApp());

    // FloatingActionButtonを押して、新しいタスクを追加
    await tester.tap(find.byType(FloatingActionButton));
    await tester.pumpAndSettle(); // ダイアログの描画を待つ

    // テキストフィールドにタスクを入力
    await tester.enterText(find.byType(TextField), 'New Task');
    await tester.tap(find.text('Add'));
    await tester.pumpAndSettle(); // タスク追加後の描画を待つ

    // タスクがリストに追加されたか確認
    expect(find.text('New Task'), findsOneWidget);
  });

  testWidgets('Todo item is removed correctly', (WidgetTester tester) async {
    await tester.pumpWidget(TodoApp());

    // 新しいタスクを追加
    await tester.tap(find.byType(FloatingActionButton));
    await tester.pumpAndSettle();
    await tester.enterText(find.byType(TextField), 'Task to remove');
    await tester.tap(find.text('Add'));
    await tester.pumpAndSettle();

    // リストにタスクが表示されていることを確認
    expect(find.text('Task to remove'), findsOneWidget);

    // 削除ボタンを押してタスクを削除
    await tester.tap(find.byIcon(Icons.delete));
    await tester.pumpAndSettle();

    // タスクが削除されたことを確認
    expect(find.text('Task to remove'), findsNothing);
  });

  testWidgets('Todo item is edited correctly', (WidgetTester tester) async {
    await tester.pumpWidget(TodoApp());

    // 新しいタスクを追加
    await tester.tap(find.byType(FloatingActionButton));
    await tester.pumpAndSettle();
    await tester.enterText(find.byType(TextField), 'Task to edit');
    await tester.tap(find.text('Add'));
    await tester.pumpAndSettle();

    // リストにタスクが表示されていることを確認
    expect(find.text('Task to edit'), findsOneWidget);

    // リストアイテムをタップして編集ダイアログを開く
    await tester.tap(find.text('Task to edit'));
    await tester.pumpAndSettle();
    await tester.enterText(find.byType(TextField), 'Edited Task');
    await tester.tap(find.text('Save'));
    await tester.pumpAndSettle();

    // タスクが編集されたことを確認
    expect(find.text('Edited Task'), findsOneWidget);
    expect(find.text('Task to edit'), findsNothing);
  });
}
Dart
flutter run
00:02 +3: All tests passed!  

テストとデバッグのコツ

テスト駆動開発(TDD)の導入

テスト駆動開発(TDD)とは、テストを先に書き、そのテストをパスするようにコードを実装していく開発手法です。これにより、バグの少ない、堅牢なコードを効率的に開発できます。

自動テストの活用

手動でのテストは時間がかかるため、自動テストを活用しましょう。Flutterでは、簡単にテストを自動化でき、CI/CDパイプラインに組み込むことも可能です。


4. まとめ

今回の記事では、Flutterの単体テスト、ウィジェットテストの基本的な使い方を解説しました。これらのツールを活用することで、アプリの品質を保ち、効率的に問題を発見し解決することができます。

次回は、いよいよリリースしていきます。

次回はこちら

初心者と学ぶFlutter第8回:アプリをiOS実機にインストール
FlutterアプリをiOS実機にインストールする方法を初心者向けに解説。アイコン設定からiOS用ビルド、Xcodeでのデプロイまで詳しく説明。

コメント

タイトルとURLをコピーしました