テストコードのデザインパターンの一つに、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(); }); }); }