Flutterにおけるテストの作法

2024/10/09に公開

はじめに

自分自身はWebエンジニア出身でFlutterでの開発経験はほぼ皆無なのですが、個人的に最近Flutterの勉強を始めています。

今回はFlutterにおいてテストはどう書いていくのがよいのか調べてみました。

公式ドキュメント

テストについては公式ドキュメントが一番よくまとまっています。概要を掴むには日本語の記事でもよいですが、まずは公式のものからあたるのが良いです。

テストのサンプルはこちらのcodelabで学ぶことができます。
https://codelabs.developers.google.com/codelabs/flutter-app-testing?hl=ja

テストの種類

Flutterにおいて、テストは3種類あります。
ユニットテスト・ウィジェットテスト・インテグレーションテストです。

各テストは下記のような内訳になります。

項目 ユニット ウィジェット インテグレーション
信頼度 最高
メンテナンスコスト 最高
依存度 とても多い
実行速度 速い 速い 遅い

テストファイルの配置場所

Flutterのテストではルートにある/testフォルダ以下にテストを作成することが慣習です。

実際にはDartのテストランナーは特定のディレクトリ構造を強制しないので、
/testフォルダ以外に配置しても、テストを実行することは可能です。

ただ/testフォルダに配置するのがデフォルトなので、多くの開発者やチームはその慣習に従っています。他の開発者のことを考え、/testフォルダ以下にテストは配置するようにしましょう。

またテストファイル名は常に_test.dartで終わる必要があります。これはテストランナーがテストを検索するときに使用する規則です。

model.dartのテストファイル名はmodel_test.dartにしましょう。

/test以下のディレクトリ構成

Flutterではソースコードは/lib以下に書いていきます。

/lib以下と/test以下は同じ階層にするのが慣習です。

lib/featureA/repository/A_repository.dartのテストのファイル階層はtest/featureA/repository/A_repository_test.dartにしましょう。

ただ中規模以上のアプリの場合、ウィジェットテストやインテグレーションテストを書きたい場合は、/test/unit、/test/widget、/test/integrationのように分けることも考えられます。

以下は一例です。

test/
  ├── unit/
  │   ├── domain/
  │   │   ├── entities/
  │   │   ├── repositories/
  │   │   └── use_cases/
  │   ├── data/
  │   │   ├── repositories/
  │   │   ├── data_sources/
  │   │   └── models/
  │   └── core/
  │       ├── utils/
  │       └── constants/
  ├── widget/
  │   ├── pages/
  │   └── widgets/
  └── integration/
      ├── app_flow/
      └── api_integration/

ユニットテスト

ユニットとは「単体の関数・メソッド・またはクラス」のことです。
以下がユニットテストのサンプルです。

テストファイルは必ずmain関数を持ちます。mainの中にテストを記述します。

import 'package:test/test.dart';
import 'package:testing_app/model/favorites.dart';

void main() {
  group('Testing App Provider', () {
    var favorites = Favorites();

    test('A new item should be added', () {
      var number = 35;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
    });

    test('An item should be removed', () {
      var number = 45;
      favorites.add(number);
      expect(favorites.items.contains(number), true);
      favorites.remove(number);
      expect(favorites.items.contains(number), false);
    });
  });
}

ウィジェットテスト

ウィジェット テストはFlutterに固有のテストであり、各ウィジェットを個別にテストすることができます。ウィジェットの表示の内容やボタンを押した時に項目が消えるか、追加されるかなどの挙動をテストします。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:provider/provider.dart';
import 'package:testing_app/models/favorites.dart';
import 'package:testing_app/screens/favorites.dart';

late Favorites favoritesList;

Widget createFavoritesScreen() => ChangeNotifierProvider<Favorites>(
      create: (context) {
        favoritesList = Favorites();
        return favoritesList;
      },
      child: const MaterialApp(
        home: FavoritesPage(),
      ),
    );

void addItems() {
  for (var i = 0; i < 10; i += 2) {
    favoritesList.add(i);
  }
}

void main() {
  group('Favorites Page Widget Tests', () {
    testWidgets('Test if ListView shows up', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      expect(find.byType(ListView), findsOneWidget);
    });

    testWidgets('Testing Remove Button', (tester) async {
      await tester.pumpWidget(createFavoritesScreen());
      addItems();
      await tester.pumpAndSettle();
      var totalItems = tester.widgetList(find.byIcon(Icons.close)).length;
      await tester.tap(find.byIcon(Icons.close).first);
      await tester.pumpAndSettle();
      expect(tester.widgetList(find.byIcon(Icons.close)).length,
          lessThan(totalItems));
      expect(find.text('Removed from favorites.'), findsOneWidget);
    });
  });
}

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

Flutterにおけるインテグレーションテストは一般にはE2Eテストと呼ばれるもので、完全にアプリの動作を再現したテストになります。

テストの書き方はウィジェットテストとほぼ同じですが、実際のプラットフォーム上で動作します。

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:how_to/main.dart';
import 'package:integration_test/integration_test.dart';

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('end-to-end test', () {
    testWidgets('tap on the floating action button, verify counter',
        (tester) async {
      // Load app widget.
      await tester.pumpWidget(const MyApp());

      // Verify the counter starts at 0.
      expect(find.text('0'), findsOneWidget);

      // Finds the floating action button to tap on.
      final fab = find.byKey(const ValueKey('increment'));

      // Emulate a tap on the floating action button.
      await tester.tap(fab);

      // Trigger a frame.
      await tester.pumpAndSettle();

      // Verify the counter increments by 1.
      expect(find.text('1'), findsOneWidget);
    });
  });
}

さいごに

今回はFlutterでテストを書き始める前に知っておきたい作法についてまとめてみました。

ロジックについてはユニットテストを書きつつ、ウィジェットテストで各ユースケースの動作のテストを書いていけると良さそうですね。

Discussion