【Flutter】3種類のテストをGithub Actionsで自動化する

【Flutter】3種類のテストをGithub Actionsで自動化するアイキャッチ 備忘録

本記事でわかること
  • Flutterの3種類のテストの実装方法
  • Flutterの3種類のテストをGithub Actionsで自動化する方法

Flutterのテストはユニット・ウィジェット・インテグレーションの3種類

Flutterには下表に示すような3種類のテストがあり、内容は以下のようになっています。

テストの種類内容
ユニットテストFlutterの機能ではないDartオンリーな部分のテストを行う。
UIとは切り離された部分のテストである。
ウィジェットテストFlutterのWidgetを操作しながらテストを行う。
ユニットテストのWidget版とイメージしてもらうと分かりやすい。
インテグレーションテストアプリ全体の統合的なテストを行う。他のテストとは異なり、
実際にエミュレータを立ち上げ、事前に設定しておいた操作が行われる。

本記事では、これらのテストを実装し、Github Actionsでテスト自動化するところまで紹介します。

テスト対象のアプリを簡単に紹介する

まず、テストを実装する対象のアプリの画面を以下に示す。

仕様は以下のような感じになっており、いかにもテストのために作られた仕様である。

仕様
  • 数字をタップすると、その数が計算結果に加算される
  • 計算結果が偶数か奇数かを表示する
  • 右下のボタンをタップすると、計算結果を0にリセットする
  • 右上のボタンをタップすると、アプリについて画面に遷移する

ユニットテストを実装する

本記事では、数字をタップしたら行われる、足し算を行う関数についてユニットテストを実装しました。

testプラグインを導入する

ユニットテストを行うためには、「flutter_test」ではなく 「test」パッケージを導入する必要があります。

dependencies:
  test: ^1.15.4

ユニットテスト用のDartファイルを作成する

プロジェクトルート>testフォルダ配下に任意のファイル名のDartファイルを作成します。

そのDartファイルに、以下のように先ほど導入してtestパッケージをインポートして、テストを書いていきます。

import 'package:flutter_ci_test/calc_model.dart';
import 'package:test/test.dart';
// これとは違うので注意!! import 'package:flutter_test/flutter_test.dart';

void main() {
  test('Calculation.add() test', () {
    // 0+0は0なことを確認する
    expect(Calculation.add(0, 0), 0);

    // 1+1は2なことを確認する
    expect(Calculation.add(1, 1), 2);

    // 負の数にも対応していることを確認する
    expect(Calculation.add(-4, 2), -2);
  });
}

main()関数の中に、『test(‘テスト名’ () { expect(hoge, hoge) });』の形でテストを書いていきます。

ユニットテストを実行する

ターミナルで以下のコマンドを叩くだけでOKです。

flutter test

ウィジェットテストを実装する

本記事では、偶数/奇数の表示が正しいことと、リフレッシュボタンをタップすることで計算結果が0になることを確認するウィジェットテストを実装しました。

「flutter_test」パッケージが導入されているか確認する

pubspec.yamlではデフォルトで記載されているはずですが、一応確認しておきます。

dev_dependencies:
  flutter_test:
    sdk: flutter

ウィジェットテスト用のDartファイルを作成する

ユニットテストと同じく、プロジェクトルート>testフォルダ配下に任意のファイル名のDartファイルを作成します。

今回は、デフォルトで作成されていた「widget_test.dart」ファイルを直接編集しました。

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

import 'package:flutter_ci_test/main.dart';

void main() {
  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
    // ウィジェットを立ち上げる
    await tester.pumpWidget(MyApp());

    /// 最初は、「計算結果:0」と表示されていることを確認する
    expect(find.text('計算結果:0'), findsOneWidget);


    /// 計算結果が偶数の時は「偶数」、奇数の時は「奇数」と表示されていることを確認する
    // (同時に各ボタンが動作することも確認する)
    await tester.tap(find.byKey(Key('1')));
    await tester.pump();
    expect(find.text('奇数'), findsOneWidget); // この時点で計算結果:1 なので奇数

    await tester.tap(find.byKey(Key('2')));
    await tester.pump();
    expect(find.text('奇数'), findsOneWidget); // この時点で計算結果:3 なので奇数

    await tester.tap(find.byKey(Key('3')));
    await tester.pump();
    expect(find.text('偶数'), findsOneWidget); // この時点で計算結果:6 なので偶数


    /// FloatingActionボタンを押すと、計算結果が、0になることを確認する
    await tester.tap(find.byIcon(Icons.refresh));
    await tester.pump();
    expect(find.text('計算結果:0'), findsOneWidget);
  });
}

ウィジェットテストでは、WidgetTesterを使って、仮想的に操作を行い、テストを実行しています。

また、『await tester.tap(find.byKey(Key(‘1’)));』のように、Keyを使って、タップするウィジェットを探していますが、これは事前にタップしたいウィジェットにKeyを仕込んでおく必要があります。

今回の場合は、アプリ側のコードで以下のようにKeyを仕込んであります。

  Widget _numberButton(int number) {
    return InkWell(
      hoverColor: Colors.orangeAccent,
      key: Key(number.toString()), // ★★★ここでKeyを設定している★★★
      onTap: () {
        setState(() {
          calcResult = Calculation.add(calcResult, number);
        });
      },
      child: Container(
        width: 100,
        height: 100,
        decoration: BoxDecoration(
          border: Border.all(),
        ),
        child: Center(child: Text(number.toString(), style: TextStyle(fontSize: 50),)),
      ),
    );
  }

ウィジェットテストを実行する

ユニットテストと同じく、以下のコマンドを叩くだけでOKです。

flutter test

インテグレーションテストを実装する

本記事では、以下の操作を仮想的に行い、インテグレーションテストとしています。

  • 起動直後の画面をスクリーンショットする
  • アプリについて画面に遷移する
  • 表示されているアプリのバージョンが期待値通りか確認する
  • アプリについて画面をスクリーンショットする
  • 起動直後の画面に戻る

flutter_driverパッケージを導入する

pubspec.yamlに以下のように記載して、flutter_driverを導入する

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_driver: // ★追加
    sdk: flutter  // ★追加

インテグレーションテスト用のDartファイルを作成する

プロジェクトルートに「test_driver」フォルダを作成し、そこに「test.dart」と「app_test.dart」を作成します。

今回はスクリーンショットを保存するために、「screenshots」フォルダも作成しました。

インテグレーションテスト準備のディレクトリ構造

まず、app.dartの中身は以下のように記載します。

import 'package:flutter_driver/driver_extension.dart';
import 'package:flutter_ci_test/main.dart' as app;

void main() {
  enableFlutterDriverExtension(); // Flutter拡張機能を有効にする
  app.main();
}

次に、app_test.dartの中で、実際のテストを記述していきます。

import 'dart:io';

import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
  group('Driver Test', () {
    FlutterDriver driver;

    setUpAll(() async {
      driver = await FlutterDriver.connect();
    });

    tearDownAll(() async {
      if (driver != null) {
        await driver.close();
      }
    });

    /// スクリーンショットを取る関数
    Future<void> _takeScreenShot(String filename) async {
      await driver.waitUntilNoTransientCallbacks();
      final pixels = await driver.screenshot();

      final file = File('./test_driver/screenshots/$filename.png');
      await file.writeAsBytes(pixels);
      print('wrote $file');
    }

    test('起動直後の画面', () async {
      final health = await driver.checkHealth();
      print(health.status);

      await _takeScreenShot('起動直後の画面');
    });

    test('アプリについて画面へ遷移して戻ってくる', () async {
      final health = await driver.checkHealth();
      print(health.status);

      await driver.tap(find.byValueKey('toAppAbout')); // アプリについて画面に遷移する
      expect(await driver.getText(find.byValueKey('appVersion')), 'アプリバージョン:1.0.0');

      await _takeScreenShot('アプリについて画面');

      await driver.tap(find.byTooltip('Back')); // 元の画面に戻る
    });
  });
}

setUpAll()でテスト開始前にアプリに接続し、tearDownAll()でテスト終了後にアプリから切断しています。その他は、他のテストと大差ない書き方です。※FlutterDriverの使い方には慣れが必要。

インテグレーションテストを実行する

シミュレータを起動した後に、ターミナルで以下のコマンドを叩くだけでOKです。

flutter drive --target=test_driver/app.dart

3種類のテストをGithub Actionsで自動化する

ここからは、Github リポジトリにプッシュした時に、自動ですべてのテストが走るように実装してみます。

まず、ワークフロー(ymlファイル)を用意する

プロジェクトルート>.github>workflowsフォルダに○○○.yamlファイルを作成します。

on句ではgithubにpushしたときにActionsが走ることを指定しています。

name: flutter_test

on:
  push:
  workflow_dispatch:

jobs:

※jobs配下にテスト実行するスクリプトを書いていきます。

ユニット・ウィジェットテストを自動化する

Github ActionsでFlutterの環境構築をしてくれる、Flutter actionを使用させていただきました。

jobs:
  test:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v2
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '2.2.0'
          channel: 'stable'
      - run: flutter pub get
      - run: flutter test

Flutterの環境構築をしたあとに、pub getして、テストを走らせているだけなので、分かりやすいと思います。

インテグレーションテストを自動化する

以下のコードで実現できます。

jobs:
  drive_test:
    runs-on: macos-latest
    strategy:
      matrix:
        device:
        - "iPhone 8 (14.4)"
        - "iPhone 11 Pro Max (14.4)"
    steps:
      - name: "List all simulators"
        run: "xcrun instruments -s"
      - name: "Start Simulator"
        run: |
          UDID=$(
            xcrun instruments -s |
            awk \
              -F ' *[][]' \
              -v 'device=${{ matrix.device }}' \
              '$1 == device { print $2 }'
          )
          xcrun simctl boot "${UDID:?No Simulator with this name found}"
      - uses: actions/checkout@v1
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '2.2.0'
          channel: 'stable'
      - name: "Run Flutter Driver tests"
        run: "flutter drive --target=test_driver/app.dart"

strategymatrixで複数のデバイスでテストするようにしていますが、一つでも構いません。

name: "Start Simulator"部分では、matrixで指定したOSに一致するものをxcrun instrumentsにより起動しています。

その後は、ローカルと同様に、インテグレーションテストを実行するコマンドを叩いているだけです。

おわりに

Github Actionsはプライベートリポジトリの場合、使用するOS、使用時間により従量課金制になっていますので、ご注意ください。

また、インテグレーションテストはテスト実行に時間がかかるので、特に注意が必要です。

なのでまずは、サンプルアプリを作って、パブリックリポジトリで遊んでみるのをおすすめします。

ソースコードはこちら

コメント

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