Flutterを使ったAndroid・iOSアプリ開発のCIパイプラインを構築する(前半) #flutter
はじめに
アプリケーションを継続的に安全に機能追加していくプラクティスとして継続的インテグレーション(CI:Continuous Integration)があります。
コードの追加、すなわちPushやPull Request / Merge Request契機でCIを実行し、その変更が既存機能を破壊していないことを確認してからマージするような仕組みにして、対象のブランチを常にクリーンに保つことでアプリケーションが常に安定して動くことを保証します。
今回はFlutterを使ったAndroidアプリ・iOSアプリケーションにおけるCIパイプラインを構築した例を紹介します。
FlutterのCIパイプラインで実行すること
一般的な CIパイプラインでやることは以下のようなものがあります。
Flutterを使ったAndroid / iOSアプリ開発では、ネイティブコードの部分を考慮して以下の4つのレイヤのCIパイプラインを実行すればよいでしょう。
それぞれ順番に見ていきます。
1. Flutterレイヤ(Dart)のCIパイプライン
FlutterレイヤではDart部分のCIパイプラインを見ていきます。Flutterではflutterコマンドの中で静的解析やユニットテストのコマンドが用意されているのでそれを実行すればよいでしょう。
静的解析
静的解析については、DartのLintがflutterコマンド経由で利用することができます。プロジェクトのrootにanalysis_options.yaml
を定義すると、そのルールに従ったLintを実行してくれます。
analysis_options.yaml
についてはflutterのレポジトリにもサンプルが用意されているのでまずはこちらを参考にするとよいでしょう。
flutter/analysis_options.yaml at master · flutter/flutter · GitHub
LintのルールはDartの公式で一覧が公開されています。
実行はflutterコマンドを実行するだけです。
$ flutter analyze Analyzing flutter_app... No issues found! (ran in 10.9s)
ユニットテスト
Dart層のユニットテストは dartの単体テストとWidgetテストの2つがあり、いずれもflutterコマンドで実行ができます。
$ flutter test
00:09 +3: All tests passed!
ユニットテストやWidgetについては公式のガイドが参考になります。
ユニットテストのカバレッジレポートも出すことができます。testコマンドに--coverage
オプションをつけて実行すると、 coverage/lcov.info
ファイルが出力されます。これをlcov
コマンドのgenhtml
でhtmlを出力することができます。
$ flutter test --coverage 00:10 +3: All tests passed! Collecting coverage information... 19ms $ genhtml -o coverage coverage/lcov.info Reading data file coverage/lcov.info Resolved relative source file path "lib/counter.dart" with CWD to "/Users/takami228/git/flutter_app/lib/counter.dart". Found 2 entries. Found common filename prefix "/Users/takami228/git/flutter_app" Writing .css and .png files. Generating output. Processing file lib/main.dart Processing file lib/counter.dart Writing directory view page. Overall coverage rate: lines......: 96.2% (25 of 26 lines) functions..: no data found
htmlファイルを開くと、以下のようなC0カバレッジのレポートを見ることができます。
2. Androidレイヤ (Kotlin/Java)のCIパイプライン
Flutterを使ったAndriodレイヤのパイプラインは基本的には通常のAndroidアプリと同様のことを実行すればよいでしょう。
メインのソースはFlutterで記述し、ネイティブレイヤのモジュールをKotlin/Javaで記述する、というケースが多いと思うのでそのモジュールに対して静的解析・単体テストを実行すれば十分だと思います。
Flutterプロジェクトのrootからandroid
フォルダに入れば通常のAndroidプロジェクトと同様に扱えるので、graldeのタスクで静的解析や単体テストを実行するように定義すればよいです。
# 静的解析を実行 $ ./graldew :app:lint # ユニットテストを実行 $ ./graldew :app:test
3. iOSレイヤ (Swift/Objective-C)のCIパイプライン
iOSレイヤもAndroidと同様に、Swift/Objectiv-Cで記述したモジュールに対しての静的解析とユニットテストを実行すればよいでしょう。
ここでは静的解析には swiftlint
、ユニットテストにはXCTestを実行するコマンドを例にあげます。XCTestを実行可能なビルドスキームを定義しておきましょう。
# 静的解析を実行 $ swiftlint lint --strict # ユニットテストを実行 $ xcodebuild test -scheme Runner-Test
4. 結合・E2Eテストレイヤ(Android / iOS)のCIパイプライン
最後は結合テスト・E2Eテストです。FlutterにはFlutter driverというE2Eテストのフレームワークが用意されています。実際にアプリを動かして、UI操作をコードで制御し、画面遷移や描画内容が正しいことを確認することができます。AppiumはまだFlutterには対応中のようなので、UIテストにはFlutter driverを利用するのがよいでしょう。
An introduction to integration testing - Flutter
flutter drive
コマンドでテスト対象のターゲットを指定することでE2Eテストが実行できます。
$ flutter drive --target=test_driver/app.dart Using device Android SDK built for x86. Starting application: test_driver/app.dart Installing build/app/outputs/apk/app.apk... 3.0s Running Gradle task 'assembleDebug'... Running Gradle task 'assembleDebug'... Done 27.2s ✓ Built build/app/outputs/apk/debug/app-debug.apk. Installing build/app/outputs/apk/app.apk... 2.7s .... [info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:63515/lShiCMdeCec=/ [trace] FlutterDriver: Isolate found with number: 3216252190998139 [trace] FlutterDriver: Isolate is paused at start. [trace] FlutterDriver: Attempting to resume isolate [trace] FlutterDriver: Waiting for service extension [info ] FlutterDriver: Connected to Flutter application. 00:02 +0: Counter App starts at 0 00:03 +1: Counter App increments the counter 00:03 +2: Counter App (tearDownAll) 00:03 +2: All tests passed! Stopping application instance.
flutter driverでは動作環境が複数ある場合は一番上位のもの上でしか実行されないため、実行対象を指定したい場合は-d
オプションでdeviceIdを指定します。
$ flutter devices 2 connected devices: Android SDK built for x86 • emulator-5554 • android-x86 • Android 10 (API 29) (emulator) iPhone 11 Pro Max • DB3DD7DC-1574-41D3-B739-B53E281F003B • ios • com.apple.CoreSimulator.SimRuntime.iOS-13-4 (simulator) $ flutter drive --target=test_driver/app.dart -d DB3DD7DC-1574-41D3-B739-B53E281F003B Starting application: test_driver/app.dart Running Xcode build... ├─Assembling Flutter resources... 24.5s └─Compiling, linking and signing... 19.8s Xcode build done. 63.0s flutter: Observatory listening on http://127.0.0.1:64370/ZIVy-qOa5uM=/ 00:00 +0: Counter App (setUpAll) [info ] FlutterDriver: Connecting to Flutter application at http://127.0.0.1:64370/ZIVy-qOa5uM=/ [trace] FlutterDriver: Isolate found with number: 1868789005249195 [trace] FlutterDriver: Isolate is paused at start. [trace] FlutterDriver: Attempting to resume isolate [trace] FlutterDriver: Waiting for service extension [info ] FlutterDriver: Connected to Flutter application. 00:02 +0: Counter App starts at 0 00:02 +1: Counter App increments the counter 00:02 +2: Counter App (tearDownAll) 00:02 +2: All tests passed! Stopping application instance.
まとめ:FlutterのCIパイプラインを構築する
以上の4つのレイヤで実行するコマンド類をシェルスクリプトにまとめて、順番に実行するようにすればFlutterのCIパイプラインは完成です。 実際にCIパイプラインを組む場合はビルド実行に冪等性をもたせる必要があるため、依存関係解決やcleanの実行も忘れずに行うようにしましょう。
flutter-ci.sh
#!/bin/sh set -x set -e # cleanを実行する flutter clean # 依存関係を解決する flutter pub get # 静的解析を実行する flutter analyze # ユニットテストを実行する flutter test --coverage
android-ci.sh
#!/bin/sh set -x set -e # フォルダを移動する cd android # cleanを実行 ./graldew :app:clean # 静的解析を実行 ./graldew :app:lint # ユニットテストを実行 ./graldew :app:test # フォルダを移動する cd ..
ios-ci.sh
#!/bin/sh set -x set -e # フォルダを移動する cd ios # cleanを実行 xcodebuild clean -scheme Runner-Test rm -rf ~/Library/Developer/Xcode/DerivedData/ # 依存関係を解決 pod install # 静的解析を実行 swiftlint lint --strict # ユニットテストを実行 xcodebuild test -scheme Runner-Test # フォルダを移動する cd ..
android-e2e-test.sh
#!/bin/sh set -x set -e # AndroidのdeviceIdを取得 ANDROID_DEVICE_ID=`flutter devices | grep "android" | cut -f2 -d "•" | tr -d " "` # AndroidのE2Eテストを実行する flutter driver --target=test_driver/app.dart -d ${ANDROID_DEVICE_ID}
ios-e2e-test.sh
#!/bin/sh set -x set -e # iOSのdeviceIdを取得 IOS_DEVICE_ID=`flutter devices | grep "ios" | cut -f2 -d "•" | tr -d " "` # iOSのE2Eテストを実行する flutter driver --target=test_driver/app.dart -d ${IOS_DEVICE_ID}
あとはこれらのシェルスクリプトを各種CIツール上で実行するように設定すればよいでしょう。
それについては長くなってきたので後半の記事で説明します。