テストコードのデザインパターンの一つに、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)のメソッドにラッピングしていきます。
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();
}
}
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();
});
});
}