はじめに
経験上会社でテスト導入の説得をしていないので、個人で導入した話を書きました。
モバイルアプリ開発において、テスト自動化は品質保証の要となります。
本記事では、SwiftとFlutterにおける最新のテスト自動化手法を、初心者にもわかりやすく解説します。
目次
- Swiftのテスト自動化
- Flutterのテスト自動化
- CI/CDとの連携
- ベストプラクティス
1. Swiftのテスト自動化
1.1 XCTestの基本
SwiftではXCTestフレームワークを使用してテストを書きます。
import XCTest
@testable import YourApp
class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUp() {
super.setUp()
calculator = Calculator()
}
func testAddition() {
// Given
let a = 5
let b = 3
// When
let result = calculator.add(a: a, b: b)
// Then
XCTAssertEqual(result, 8)
}
}
次に1行ずつ解説していきます。
import XCTest
XCTestフレームワークを使用するために必要なインポート文です。このフレームワークにはテストに必要な全ての機能が含まれています。
@testable import YourApp
テスト対象のアプリケーションモジュールをインポートします。@testableキーワードにより、internal修飾子で宣言されたクラスやメソッドにもアクセスできるようになります。
class CalculatorTests: XCTestCase {
XCTestCaseを継承したテストクラスを定義します。
全てのテストケースはこのクラスのサブクラスとして実装します。
var calculator: Calculator!
テスト対象となるCalculatorクラスのインスタンス変数を宣言します。!はImplicitly Unwrapped Optionalを示し、setUp()で必ず初期化されることを表します。
override func setUp() {
各テストメソッドが実行される前に呼ばれるセットアップメソッドです。テストに必要な初期状態を設定します。
super.setUp()
親クラスのsetUpメソッドを呼び出します。これにより、XCTestCaseの標準的な初期化処理が実行されます。
calculator = Calculator()
テスト用のCalculatorインスタンスを生成します。各テストメソッドの実行前に新しいインスタンスが作られるため、テスト間の独立性が保たれます。
func testAddition() {
加算機能をテストするメソッドです。メソッド名は必ず"test"で始める必要があります。
let a = 5
テストで使用する1つ目の入力値を定義します。
let b = 3
テストで使用する2つ目の入力値を定義します。
let result = calculator.add(a: a, b: b)
テスト対象のメソッドを実行し、結果を取得します。
XCTAssertEqual(result, 8)
計算結果が期待値(8)と一致するかを検証します。一致しない場合、テストは失敗となります。
1.2 UITestingの実装
class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
super.setUp()
app.launch()
}
func testLoginFlow() {
// UIの要素にアクセス
let emailTextField = app.textFields["email"]
let passwordTextField = app.secureTextFields["password"]
let loginButton = app.buttons["loginButton"]
// テストの実行
emailTextField.tap()
emailTextField.typeText("test@example.com")
passwordTextField.tap()
passwordTextField.typeText("password123")
loginButton.tap()
// ログイン成功の確認
XCTAssertTrue(app.staticTexts["welcomeMessage"].exists)
}
}
次に1行ずつ解説していきます。
class LoginUITests: XCTestCase {
UIテスト用のクラスを定義します。UIテストもXCTestCaseを継承します。
let app = XCUIApplication()
テスト対象のアプリケーションインスタンスを作成します。このインスタンスを通じてUIの操作を行います。
override func setUp() {
UIテスト実行前の準備を行うメソッドです。
super.setUp()
親クラスの初期化処理を実行します。
app.launch()
テスト対象のアプリケーションを起動します。
let emailTextField = app.textFields["email"]
"email"という識別子を持つテキストフィールドを取得します。この識別子はStoryboardやコードで設定されている必要があります。
let passwordTextField = app.secureTextFields["password"]
"password"という識別子を持つセキュアテキストフィールド(パスワード入力用)を取得します。
let loginButton = app.buttons["loginButton"]
"loginButton"という識別子を持つボタンを取得します。
emailTextField.tap()
メールアドレス入力フィールドをタップします。これにより入力フォーカスが設定されます。
emailTextField.typeText("test@example.com")
メールアドレスフィールドにテキストを入力します。実際のキーボード入力をシミュレートします。
passwordTextField.tap()
パスワード入力フィールドをタップしてフォーカスを移動します。
passwordTextField.typeText("password123")
パスワードフィールドにテキストを入力します。
loginButton.tap()
ログインボタンをタップしてログイン処理を実行します。
XCTAssertTrue(app.staticTexts["welcomeMessage"].exists)
ログイン成功後に表示される"welcomeMessage"という識別子を持つテキストが存在するかを確認します。
2. Flutterのテスト自動化
2.1 単体テスト
import 'package:test/test.dart';
import 'package:your_app/calculator.dart';
void main() {
group('Calculator Tests', () {
late Calculator calculator;
setUp(() {
calculator = Calculator();
});
test('addition test', () {
// Given
final a = 5;
final b = 3;
// When
final result = calculator.add(a, b);
// Then
expect(result, 8);
});
});
}
次に1行ずつ解説していきます。
import 'package:test/test.dart';
Dartの標準テストパッケージをインポートします。これにより、test関数やexpect関数などのテスト用APIが使用可能になります。
import 'package:your_app/calculator.dart';
テスト対象のCalculatorクラスが定義されているファイルをインポートします。
void main() {
テストのエントリーポイントとなる関数です。この中にすべてのテストケースを記述します。
group('Calculator Tests', () {
関連するテストケースをグループ化します。テストの整理と可読性向上に役立ちます。
late Calculator calculator;
テストで使用するCalculatorインスタンスを宣言します。lateキーワードは、変数が使用される前に必ず初期化されることを示します。
setUp(() {
各テストケース実行前に呼び出される準備用の関数です。
calculator = Calculator();
テスト用のCalculatorインスタンスを生成します。各テストの前に新しいインスタンスが作られます。
test('addition test', () {
加算機能をテストするケースを定義します。第一引数はテストの説明です。
final a = 5;
テストで使用する1つ目の入力値を定義します。
final b = 3;
テストで使用する2つ目の入力値を定義します。
final result = calculator.add(a, b);
テスト対象のメソッドを実行して結果を取得します。
expect(result, 8);
2.2 Widget Testing
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/login_page.dart';
void main() {
testWidgets('Login form test', (WidgetTester tester) async {
// ウィジェットのビルド
await tester.pumpWidget(MaterialApp(home: LoginPage()));
// テストの実行
await tester.enterText(
find.byKey(Key('emailField')),
'test@example.com'
);
await tester.enterText(
find.byKey(Key('passwordField')),
'password123'
);
await tester.tap(find.byType(ElevatedButton));
await tester.pump();
// 検証
expect(find.text('Welcome!'), findsOneWidget);
});
}
次に1行ずつ解説していきます。
testWidgets('Login form test', (WidgetTester tester) async {
ウィジェットテストを定義します。WidgetTesterを使用してウィジェットの操作をシミュレートします。
await tester.pumpWidget(MaterialApp(home: LoginPage()));
テスト用にLoginPageウィジェットをビルドします。MaterialAppでラップすることで、必要なコンテキストを提供します。
await tester.enterText(
find.byKey(Key('emailField')),
'test@example.com'
);
emailFieldという識別子を持つテキストフィールドにメールアドレスを入力します。
await tester.enterText(
find.byKey(Key('passwordField')),
'password123'
);
passwordFieldという識別子を持つテキストフィールドにパスワードを入力します。
await tester.tap(find.byType(ElevatedButton));
ElevatedButtonタイプのウィジェットをタップします。
await tester.pump();
ウィジェットツリーを再構築し、アニメーションの1フレームを進めます。
expect(find.text('Welcome!'), findsOneWidget);
'Welcome!'というテキストを持つウィジェットが1つだけ存在することを確認します。
2.3 統合テスト (Integration Tests)
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:your_app/main.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('End-to-end test', (WidgetTester tester) async {
await tester.pumpWidget(MyApp());
// ログインフロー
await tester.tap(find.byKey(Key('loginButton')));
await tester.pumpAndSettle();
// ホーム画面の確認
expect(find.text('Home'), findsOneWidget);
});
}
次に1行ずつ解説していきます。
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
統合テストのための特別なバインディングを初期化します。これにより、実機やエミュレータ上でのテストが可能になります。
await tester.pumpWidget(MyApp());
アプリケーション全体をテスト環境にビルドします。
await tester.tap(find.byKey(Key('loginButton')));
loginButtonという識別子を持つボタンをタップします。
await tester.pumpAndSettle();
すべてのアニメーションが完了し、ウィジェットツリーが安定するまで待機します。
expect(find.text('Home'), findsOneWidget);
'Home'というテキストを持つウィジェットが表示されていることを確認し、ログインが成功したことを検証します。
3. CI/CDとの連携
3.1 GitHub Actionsの設定例
name: Test Automation
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
# Swiftテスト
- name: Run Swift Tests
run: |
xcodebuild test -scheme YourApp -destination 'platform=iOS Simulator,name=iPhone 15'
# Flutterテスト
- name: Set up Flutter
uses: subosito/flutter-action@v2
- name: Run Flutter Tests
run: |
flutter test
4. ベストプラクティス
4.1 テスト設計のポイント
-
Given-When-Thenパターンを使用
- テストの前提条件を明確に
- アクションを実行
- 結果を検証
-
テストの独立性を保つ
- 各テストは他のテストに依存しない
- setUp()とtearDown()を適切に使用
-
モックとスタブの活用
- 外部依存を適切に分離
- テストの実行速度を改善
4.2 効率的なテスト実装
-
テストピラミッドの考慮
- 単体テスト > 統合テスト > UIテスト
- コストと効果のバランス
-
自動化の範囲
- クリティカルなパスを優先
- ユーザーシナリオに基づく選択
まとめ
テスト自動化は、アプリケーションの品質を保証する重要な要素です。SwiftとFlutterそれぞれの特性を活かしたテスト実装により、効率的な開発サイクルを実現できます。