アジャイルテスティングのイベントに登壇

Ultimate Agilist Tokyoの10年越しの縁で藤原大さんから声をかけていただき、会社にも許可をもらって以下のアジャイルテスティングのイベントに登壇することになりました。

人類よ!これがアジャイルテスティングだ!QAテックリードが語るアジャイルQAの実践とは何か? - connpass

現在、アジャイル開発プロジェクトでテストのテックリードとしてテスト活動に従事しているほか、ソフトウェア開発でのTPSの推進を手掛けています。そのあたりのお話ができればと考えています。テーマに興味のある方がいたら参加いただけると幸いです。

世の中のアジャイルテスティングの定義と原則

アジャイルテスティングの定義や原則には、いくつかのバリエーションが存在します(大きく「アジャイル原則に適合したソフトウェアテスティング」or「アジャイルに適合したソフトウェアテスティング」の2つに分けられます)。今回は情報の整理のため、有力な文献をいくつかピックアップしました。

実践アジャイルテスト

実践アジャイルテスト テスターとアジャイルチームのための実践ガイド

アジャイルテスティングの原典の一つのように扱われる本書では、「アジャイルテスティングの定義とは」のように定義を簡潔に説明している記述はありません。
ただ参考にできる記述として、「アジャイルテスターの定義」を次のように解説しています。

アジャイルテスターとは、変化に対応し、技術担当の人や業務担当の人たちと共同作業でき、テストのコンセプトを理解して要求を文書化し開発をリードできる、プロフェッショナルなテスターです。アジャイルテスターの特徴は、高い技術力を持ち、メンバーと共同作業を心得てテストの自動化を行うことです。アジャイルテスターはまた経験豊富な探索的テスターでもあります。顧客が何をしたいかを常に気にかけており、顧客のソフトウェア要求を深く理解することができます。

基本的に、アジャイルの原則に沿ったテスティングを、アジャイルテスティングと定義しています。
またアジャイルテスティングの原則として、次を説明しています。

  • 継続的にフィードバックする
  • 顧客へ価値を提供する
  • 対面でのコミュニケーションを可能にする
  • 勇気を持つ
  • シンプルを心がける
  • 継続的な改善を実践する
  • 変化に対応する
  • 自分を律する
  • 人に焦点を当てる
  • 楽しむ

実践アジャイルテストの解説で特徴的なのは、アジャイルテスティングの実践においては、開発手法がアジャイルでなくてもよいと言及していることです。例えば「アジャイルテストとは、単にアジャイルプロジェクトにおけるテストではありません。探索的テストなどいくつかのテストは、プロジェクトがアジャイルでもそうでなくても、本質的にアジャイルです」といった解説がなされています。

More Agile Testing

More Agile Testing: Learning Journeys for the Whole Team

前述した実践アジャイルテストの補足および発展的内容を解説した本です。こちらもアジャイルテスティングの原典の1つとみなされる場合があります。
本書では「アジャイルテストの定義・原則とは」のように定義や原則を簡潔に解説している記述はありません。ただ著者が同じであることから、前述の実践アジャイルテストの原則を踏襲した解説となっています。

Agile Testing Condensed

Agile Testing Condensed… Yuya Kazama 著 et al. [PDF/iPad/Kindle]

この書籍は、前述の実践アジャイルテストとMore Agile Testingの内容の要点を解説した本です。執筆時点で日本語にてアジャイルテストを学ぼうとする場合で、第一に推薦すべき本となっています。
この本では、何度も聞かれるから、ということで、「アジャイルテストの定義」として次の解説を追加しています。

始まりからデリバリーまで、そしてそれ以降も継続的に実施される協調的なテストの実践により、お客様への価値の頻繁な提供をサポートします。テスト活動は、高速なフィードバックループを用いて理解を検証しながら、プロダクトの品質を築くことに重点を置いています。このプラクティスは、品質に対するチーム全体の責任という考え方を強化し、サポートします。

アジャイルテスティングの原則の解説も行っていますが、こちらは前述の実践アジャイルテストと同じ内容となっています。

Testing Manifesto

The Testing Manifesto | Growing Agile

Growing Agileでは、守るべきテスティングの原則として、Testing Manifestoという項目を次のように定義しています。この原則名にはアジャイルという言葉が含まれていませんが、「アジャイルをうまく実践するための知見を解説する」という文脈の中で記述されているものであり、アジャイルテスティングの原則の一つとして見なすことができます。

私たちは下記を大切にします:

  • 最後にテストするよりもずっとテストし続ける
  • バグの発見よりもバグの防止
  • 機能性をチェックするよりもチームが理解している価値をテストする
  • システムを破壊するよりも最高のシステムを構築する
  • テスターの責任よりも品質に対するチームの責任

※翻訳は前述のAgile Testing Condensed Japanese Editionから引用。

なおAgile Testing Condensedでも、アジャイルテスティングの原則の一つとしてこのマニフェストを引用しています。

JSTQB アジャイルテスト担当者

http://jstqb.jp/dl/JSTQB-SyllabusFoundation-AgileExt_Version2014.J01.pdf

ISTQB/JSTQBではアジャイルテスト担当者のシラバスを作成しており、そこでアジャイルテスティングに関する用語や概念を解説しています。ISTQB/JSTQBシラバスは世界共通の用語や概念として参照されることが多いため、今回簡単に触れます。

JSTQBアジャイルテスト担当者シラバスでは、「アジャイルテスティングの定義・原則とは」のような簡潔な解説を行っていません。
ただ、「アジャイルテスティング」=「アジャイルプロジェクトに適したテスティング」のような定義で一貫しています。
また記述が分散しているため引用は割愛しますが、開発活動のと関わり合い、テストに関する成果物、テストレベル、テスト成果物の構成管理など、いくつかのトピックごとに従来型テストと対比する形で、アジャイルテスティングの原則を解説しています。そこでも「アジャイルテスティングの原則」=「アジャイルプロジェクトに適したテストの原則」のような解説で一貫しています。「アジャイルテスティングは開発がアジャイルであることが前提」という立ち位置です。

なお近しい解説として、アジャイルテスト担当者の原則を、「持つべきスキル」として次のように解説しています。

チームメンバおよびステークホルダに対して、建設的かつソリューション指向で向き合う
プロダクトについて、批判的で、品質指向の、懐疑的な思考を発揮する
ステークホルダから情報を積極的に入手する(ドキュメント化された仕様に全面的に頼ることはしない)
テスト結果、テスト進捗およびプロダクト品質を正確に評価して報告する
テスト可能なユーザストーリー、特に受け入れ基準について定義するために、顧客の代表者やステークホルダと共に効果的に働くチーム内で協調し、プログラマおよび他のチームメンバとペアになって作業する
テストケースの変更、追加、または改善を含めて、変更に迅速に対応する
テスト担当者側での作業を計画し、準備する

Modern Testing Principles

Modern Testing – Not that modern, and not that much about testing

「Modern Testing Principles」はテスティングが重視すべき原則として定義されたもので、次の7項目で構成されます。アジャイルに限定されない原則ですが、アジャイルテスティングと方向性が一致しているものとして、アジャイルテスティングでも守るべき原則として解説されているのを度々見るため紹介します。

  1. Our priority is improving the business.
  2. We accelerate the team, and use models like Lean Thinking and the Theory of Constraints to help identify, prioritize and mitigate bottlenecks from the system.
  3. We are a force for continuous improvement, helping the team adapt and optimize in order to succeed, rather than providing a safety net to catch failures.
  4. We care deeply about the quality culture of our team, and we coach, lead, and nurture the team towards a more mature quality culture.
  5. We believe that the customer is the only one capable to judge and evaluate the quality of our product.
  6. We use data extensively to deeply understand customer usage and then close the gaps between product hypotheses and business impact.
  7. We expand testing abilities and knowhow across the team; understanding that this may reduce (or eliminate) the need for a dedicated testing specialist.

テスタビリティ(試験性)の拡張を実現する実装

前エントリ「テスタビリティ(試験性)を確保するための設計方針 - 千里霧中」の補足として、「テスタビリティ(試験性、Testability)を拡張可能にする」の実装について解説します。

拡張を実現する手段の一つに接合部(Seam)があります。今回は実装例として、接合部の一種である、メソッドインジェクションを使ってテスタビリティを拡張可能にする例を示します。

対象コード

次のtarget_method()をテストする場合について考えます。
target_method()は、device_controller_run()を呼び出して、外部デバイスを制御するメソッドです。

def device_controller_run(status):
    ...副作用を持つコード...

def target_method():
    ...
    device_controller_run(DeviceStatus.RUNNING)
    ...

テスタビリティを拡張可能にする

このtarget_method()をテスタビリティ拡張可能にします。その実現手段に、今回は次のようにメソッドインジェクションを導入します。

class DeviceController:
    def run(self, status):
    ...副作用を持つコード...

def target_method(device_controller):
    ...
    device_controller.run(DeviceStatus.RUNNING)
    ...

メソッドインジェクションは、対象の依存要素を、メソッド呼び出し時に注入するイディオムです。上記では、メソッドの引数としてDeviceControllerオブジェクトを渡し、それ経由で外部デバイス制御メソッドを呼び出すように変更しています。

メソッドインジェクションのような接合部を設けると、接合部を起点にして、後付けでテスタビリティを拡張できるようになります。

テスタビリティを拡張してユニットテストを実現する

ここで、target_method()に対するユニットテストを記述します。今回のユニットテストでは、target_method()が、適切な引数で外部デバイス制御メソッドを呼び出していることを確認します。

最初のtarget_method()の実装例では、このユニットテストを実装できるかは不明です。しかし、前述のようにメソッドインジェクションの導入でテスタビリティを拡張可能にしておくと、次のようなアプローチでテスタビリティを拡張して、ユニットテストを実現できるようになります。

class DeviceController:
    def run(self, status):
    ...副作用を持つコード...

class DeviceControllerWithTestability(DeviceController):
    def __init__(self):
        self._status = DeviceStatus.STOP

    def run(self, status):
        self._status = status

    def status_is_running(self):
        return self._status == DeviceStatus.RUNNING

def target_method(device_controller):
    '''テスト対象'''
    ...
    device_controller.run(DeviceStatus.RUNNING)
    ...
    
def test_target_method():
    '''ユニットテスト'''
    device_controller = DeviceControllerWithTestability()
    target_method(device_controller)
    assert device_controller.status_is_running()

上記では、テスト対象target_method()が依存するDeviceControllerを、DeviceControllerWithTestabilityに置換し、必要なテスタビリティを拡張しています。
テスタビリティの拡張ポイントは、DeviceControllerの副作用を解消する点、(やや過剰ですが)外部デバイス制御メソッドの引数をチェックするAssertionMethodを提供する点となります。

参考文献

接合部を含む、テスタビリティを拡張可能にするイディオムやパターンの解説としては、以下の書籍が充実しています。

レガシーコード改善ガイド | マイケル・C・フェザーズ, 平澤章, 越智典子, 稲葉信之, 田村友彦, 小堀真義, ウルシステムズ株式会社, ウルシステムズ株式会社 | コンピュータ・IT | Kindleストア | Amazon

テスタビリティ(試験性)を確保するための設計方針

テスタビリティ(試験性、テスト容易性)は「どれだけ容易にテストできるか」「どれだけテストを実現できるか」の度合いを示す品質特性です。
実践ソフトウェア・エンジニアリングの解説から引用すると、テスタビリティは次の特性から構成されます。

  • 実行円滑性(Operability)
    • テストの実行しやすさ。例えば、テスト実施をブロックする要因が少ないか。
  • 観測容易性(Observability)
    • テスト対象の観測のしやすさ。
  • 制御容易性(Controllability)
    • テスト対象の操作のしやすさ。
  • 分解容易性(Decomposability)
    • テスト対象の分離や分割のしやすさ。例えばテストのためにテスト対象を切り出しやすいか。
  • 単純性(Simplicity)
    • テスト対象の単純性。例えば機能がシンプルで必要なテストが少ないか。
  • 安定性(Stability)
    • テストに影響を与える変更の少なさ。
  • 理解容易性(Understandability)
    • テスト対象の理解しやすさ。

※この他にも、文献によってはプロジェクトの制約度合い(e.g.テストに十分なコストを確保しているか)、テストエンジニアのスキルといったテスト対象の外の特性もテスタビリティに含めているものがあります。

今回は、このテスタビリティを確保するための代表的な設計方針を解説したいと思います。

前提:様々なテストそれぞれにとってのテスタビリティがある

本題に入る前の前提の話です。テスタビリティは、あらゆるソフトウェアテストに関わる品質特性です。手動テスト、自動テスト、あるいはGUIを通したテスト、APIを通したテスト、コードレベルのテストなど種類を問わず、各々のテストにとって、それぞれのテスタビリティがあります。

例えば観測容易性を例をとります。テスタビリティの実装は、テストの種類ごとに次のように変化します。

  • 手動のシステムテストでの優れた観測容易性の実装例
    • 必要なエラーや実行情報を得るためのUIが提供されている
    • テストの前提条件(構成管理情報)を確認するためのUIが提供されている
  • APIテストでの優れた観測容易性の実装例
    • 必要な内部情報を得るためのAPIが揃っている
  • ユニットテストでの優れた観測用意性の実装例
    • 間接的な出力がなく、テストコードから必要な出力を直接簡単に参照できる

以降で解説する設計方針についても、具体的な実装は対象のテストの種類によって異なります。

テスタビリティを確保するための設計方針

テスタビリティを低下させる要因の影響範囲を小さくし、分離・置換できるようにする

テスタビリティを低下させる要因として、以下のような存在があります。

テスタビリティ確保の点では、こうしたテストの支障となるコンポーネントへの依存箇所を最小化すると、テスト可能な範囲が広がります。例えば以下のような対策が有効です。

  • 実行円滑性に劣るコンポーネント群は、Facadeパターンで簡易化したインターフェースを通して制御可能にする。
  • 安定性に劣るコンポーネントは、コンポーネントをなるべく局所化した上で、ラッピングして他との依存性を削減する。例えばUIデザインが頻繁に変更されるなら、UI層を最小化・分離するなど。

さらに、上記のテスタビリティを低下させる要素は、後述する接合部を設けて分離・置換できるようにすると、テスタビリティ確保の助けになります。例えば観測容易性に劣るコンポーネントを、観測手段を埋め込んだTest Doubleに置換できるようにするといった工夫です。

結合度を低く、凝集度を高く

一般的な設計の方針として、コンポーネント間の結合度を低くし、凝集度を高くすることが推奨されていますが、この方針はテスタビリティの改善にも繋がります。
結合度を低くするように設計・実装すると、テスタビリティのうち分解容易性が向上します。凝集度を高くするように設計・実装すると、テスタビリティのうちの単純性や理解容易性が高まります。

特に結合度を低く保つ設計については、設計の要所に接合部(Seam)を設けることが重要です。接合部は、特定のコンポーネントを切り離して他に置換できるようにするための仕組みです。依存性の逆転(Dependency Inversion)、Link Seam(ビルドを分割しリンク時に切り替えられるようにする)などがあります。接合部があると、テスト対象を切り離したり、テストの障害となるコンポーネントをTest Doubleに置換したりすることができるようになり、テストにとって十分な結合性の低さを確保できます。

テストにとって十分な観測点、制御点を設ける

観測点(Observation Point)はテストの出力を得るための手段です。その充実度が観測容易性に直結します。制御点(Control Point)はテスト対象を操作するための手段です。その充実度が制御容易性に直結します。

テスタビリティの確保においては、プロジェクトのそれぞれのテストが求める観測容易性、制御容易性の要求を識別して、それを満たす観測点・制御点を設けることが重要になります。
例えばテストに必要な観測・制御ができるようにテスト用インターフェースを設ける、テストで観測すべき情報がすべて盛り込まれるようにログ設計する、テストにとって必要な情報が得られるようにエラーなど情報通知UIを設ける、といった工夫が有効になります。

テスタビリティを拡張可能にする

後からテスタビリティを拡張可能にすると、テスタビリティを確保できなくても、テストを実施するタイミングで必要なテスタビリティを後から確保できるようになります。
また、開発成果物が将来レガシーコード化する備えの点でも、各所にテスタビリティが注入可能なポイントを実装しておくのが有効です。その備えを行っておくと、将来レガシーコード化しても、テスタビリティを注入して必要なリグレッションテストを構築し、そのテスト使って脱レガシーコードのためのリファクタリングを行うといった対策が打てるようになります。

テスタビリティを拡張可能にする手段としては、例えば前述した接合部があります。接合部を設けると、テストが依存するコンポーネントを、観測・制御手段付きに改造したコンポーネントに置換できるようになります。

テストでとり得る条件を制限する

テストの入出力のパラメータや値が少ないと、テスタビリティのうちの単純性が向上するほか、観測や制御が容易になります。
このパラメータや値の削減には、次のような工夫が有効になります。

  • 変数やメソッドなどのスコープを狭め、グローバルな依存関係を削減する
  • 冗長な引数や戻り値をなくす
  • 内部状態をシンプルに保つ
  • 副作用の発生可能性をなくす
  • 型でとり得る値を制限する

また、値の組み合わせなど、テストでとり得る条件を削減すると、同じく単純性が向上します。この実現のためには、前述のパラメータや値の削減のほか、次のような工夫が有効になります。

  • 制御フローをシンプルに保つ。例えばエラーチェックを冒頭で行ってネストを浅くするなど。これによりデシジョンテーブルの圧縮やCFD法の適用などが可能になる。
  • ロックなどの排他処理や割り込み保護を適切に行い、タイミングの組み合わせを削減する
  • 禁則の組み合わせを、実装上の工夫で実現不能にする
  • DRYを推進する

品質特性のバランスを取る

テスタビリティは、一部の品質特性とトレードオフの関係を持ちます。代表例は、性能効率性やセキュリティです。
セキュリティを例に取ると、例えばテスト用に設けたAPIセキュリティホールになるといった問題が、トレードオフの関係例です。

テスタビリティの確保にあたっては、テスタビリティとトレードオフになる品質を特定して、両者を両立する設計を見つけ出す必要があります。
例えばテスタビリティとセキュリティのトレードオフの場合ですと、次のようなアプローチが設計として有効になります。

  • トラストバウンダリを設け、その内部にテストインターフェースを設ける
  • テスト用とデプロイ用を安全に切り替えられるよう、コードやビルドシステムを工夫する

設計やコードをリーダブルに保つ

理解しやすい命名をするといった、リーダブルにするための設計やコードの原則は、単純性や理解容易性を改善し、テスタビリティを底上げします。

Flutter Integration Testingでは、testパッケージではなくSDKのintegration_testパッケージを使う

 現在、FlutterのIntegration Testでは、flutter_driverパッケージとtestパッケージの組み合わせではなく、SDKのintegration_testパッケージでテストコードを記述することが公式で推奨されています。

An introduction to integration testing | Flutter

Note: The integration_test package is now the recommended way to write integration 
tests. See the Integration testing page for details.

 SDKのintegration_testパッケージは、先月stable版がリリースされたFlutter2から利用可能になっているものです。

 integration_testパッケージは旧来のパッケージと比べて、次のようなメリットがあります。

  • Firebase Test Labでの実行をサポート
  • flutter_test APIを使って、Widget Testと同等の構文でIntegration Testを記述できる
  • 旧来のFlutter Driveコマンドに対応していて、物理デバイスでもエミュレータでもIntegration Testを実行できる

 今回はこのintegration_testパッケージを使った簡単なテストコードを示します。

pubspec.yaml

dev_dependencies:
  ...
  integration_test:
    sdk: flutter
  flutter_test:
    sdk: flutter
  ...

テストコード

 旧来のIntegration Testのテストコードは次のようになります

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

void main() {
  FlutterDriver driver;

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

  tearDownAll(() async {
    await driver.close();
  });

  group('E2E Test for Login', () {
    test('authenticate a user', () async {
      await driver.tap(find.byValueKey('usernameTextField'));
      await driver.enterText('hoge hoge');

      await driver.tap(find.byValueKey('passwordTextField'));
      await driver.enterText('sample password');

      await driver.tap(find.byValueKey('login'));

      final indexMenuFinder = find.text('Execute');
      await driver.waitFor(indexMenuFinder);
      expect(await driver.getText(indexMenuFinder), 'Execute');
    });
  });
}

 integration_testパッケージを使用すると、上記のテストコードは次のように記述できます。Widget Testと同じようなスタイルになります。

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

import 'package:flutter_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('E2E Test for Login', () {
    testWidgets('authenticate a user', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      await tester.enterText(find.byKey(Key('usernameTextField')), 'hoge hoge');
      await tester.enterText(find.byKey(Key('passwordTextField')), 'sample password');

      await tester.tap(find.byKey(Key('login')));

      await tester.pumpAndSettle();

      expect(find.text('Execute'), findsOneWidget);
    });
  });
}

テストコードのデザインパターン:Robotパターン

 テストコードのデザインパターンの一つに、Robotパターン(Testing Robotパターン、Robot Testingパターン)という汎用的なパターンがあります。

Presentation: Testing Robots - Jake Wharton
Robot Pattern Testing for XCUITest | by Rob Whitaker | Capital One Tech | Medium

 Robotパターンは、テスト対象のセグメンテーション(例えば画面)ごとにRobotオブジェクトを用意し、テストコード上にテストのWhat(テストしたい振る舞いや仕様)を、RobotオブジェクトにテストのHow(テスト対象の操作や期待値比較の詳細な実装)を実装するものです。これによりテストコードの可読性や再利用性を向上させることを目的とします。

 このパターンは、テストコード上にWhatを、PageオブジェクトにHowを実装する、一般的なPage Objectパターンと目的やアプローチが類似しています(ただRobotパターンは、Robotオブジェクト内にAssertion機能を実装するのを許容するという違いがあります。Page ObjectパターンはPageオブジェクトでのAssertion実装を推奨していません)。

 今回はFlutterアプリを対象に、このRobotパターンの実装例を示します。

テスト対象のサンプル

 HOME画面に次のようなログイン画面を表示する、単純なログイン機能を有したFlutterアプリを対象にします。今回は、このアプリを対象に、ログインして、Executeという内部機能実行ボタンが表示されることを確認するFlutter Integration Testを実装します。


Robotパターンを使用しないテストコード

 Robotパターンを使用せずにFlutter Integration Testを実装した例を次に示します。

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

import 'package:flutter_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  group('E2E Test for Login', () {
    testWidgets('authenticate a user', (WidgetTester tester) async {
      app.main();
      await tester.pumpAndSettle();

      await tester.enterText(find.byKey(Key('usernameTextField')), 'hoge hoge');
      await tester.enterText(find.byKey(Key('passwordTextField')), 'sample password');

      await tester.tap(find.byKey(Key('login')));

      await tester.pumpAndSettle();

      expect(find.text('Execute'), findsOneWidget);
    });
  });
}

 テストコードにはusernameTextFieldといったキー名など、テスト対象の詳細な操作がハードコーディングされています。

Robotパターンを使用したテストコード

 次にRobotパターンを使用してFlutter Integration Testを実装した例を次に示します。

 まず画面ごとにRobotオブジェクトを用意します。Robotオブジェクトでは、テスト対象の操作や期待値比較についての詳細な実装(How)を、わかりやすい名前(What)のメソッドにラッピングしていきます。

// login_robot.dart(ログイン画面のRobotクラス)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class LoginRobot {
  const LoginRobot(this.tester);

  final WidgetTester tester;

  Future<void> enterUserName(String username) async {
    await tester.enterText(find.byKey(Key('usernameTextField')), 'hoge hoge');
  }

  Future<void> enterPassword(String password) async {
    await tester.enterText(find.byKey(Key('passwordTextField')), 'sample password');
  }

  Future<void> tapLoginButton() async {
    await tester.tap(find.byKey(Key('login')));
    await tester.pumpAndSettle();
  }
}
// index_robot.dart(ログイン後画面のRobotクラス)
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

class IndexRobot {
  const IndexRobot(this.tester);
  final WidgetTester tester;

  Future<void> checkUserIsOnTheIndexScreen() async {
    expect(find.text('Execute'), findsOneWidget);
  }
}

 次にテストコードです。Robotオブジェクトを経由してテスト対象を操作することにより、テストコードは次のように実装できます。usernameTextFieldといった内部のキーへの直接の依存がなくなり、テストしたい振る舞いや仕様が端的に表現されたコードになります。

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

import 'robots/login_robot.dart';
import 'robots/index_robot.dart';

import 'package:flutter_app/main.dart' as app;

void main() {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();
  group('E2E Test for Login', () {
    testWidgets('authenticate a user', (WidgetTester tester) async {
      final loginRobot = LoginRobot(tester);
      final indexRobot = IndexRobot(tester);
      app.main();
      await tester.pumpAndSettle();

      await loginRobot.enterUserName('hoge hoge');
      await loginRobot.enterPassword('sample password');
      await loginRobot.tapLoginButton();
      await indexRobot.checkUserIsOnTheIndexScreen();
    });
  });
}

Flutterの自動テストをGithub Actionsで実行する

Flutterのユニットテストウィジェットテスト、インテグレーションテスト(エミュレータを使ったテスト)をGithub Actionsで実行する方法についてです。

対象のディレクトリ構成

ユニットテストウィジェットテストのワークフロー

上記ディレクトリファイルにて、testディレクトリのユニットテストウィジェットテストをGithub Actionsで実行する場合、次のようなワークフロー定義ファイルflutter_test.yamlを作成します。
push、プルリクをトリガに、単にflutter実行用のアクション上で、テスト実行コマンドを実行しています。

name: flutter component test

on: [push, pull_request]

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

インテグレーションテストのワークフロー

次にtest_driverディレクトリのインテグレーションテストをGithub Actionsで実行する場合、次のようなワークフロー定義ファイルflutter_integration_test.yamlを作成します。
この例では「iPhone 12 Pro Max (14.2)」「iPhone 12 (14.2)」のシミュレータでテストを実行します。処理としては、xcrunで利用可能なシミュレータ一覧を取得し、awkでそこから対象のUDIDを取得してエミュレータを起動します。そしてflutter driveコマンドでテスト対象・テストコードを実行します。

name: flutter integration test

on: [push, pull_request]

jobs:
  integration_test:
    strategy:
      matrix:
        device:
          - "iPhone 12 Pro Max (14.2)"
          - "iPhone 12 (14.2)"
      fail-fast: false
    runs-on: macos-latest
    steps:
      - name: "start simulator"
        timeout-minutes: 10
        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@v2
      - uses: subosito/flutter-action@v1
        with:
          channel: 'stable'
      - run: "flutter drive --target=test_driver/app.dart"