はじめに
オライリーの入門 継続的デリバリーを読みましたので、Flutterで導入するならどのような感じになるか試してみました。
ツールにはGithubActionsを使うのが簡単で良さそうだったので使用しています。
やること
入門 継続的デリバリーでは、継続的デリバリーで実行することは以下のように書かれています。
- リンターでコードの品質を高める
- 単体テスト
- 結合テスト
- E2Eテスト
- テストカバレッジの計測
- ビルド
- パブリッシュ
- デプロイ
Flutterに置き換えると以下のようになると思います。
- リンターでコードの品質を高める
- Unitテスト、Widgetテスト、Goldenテスト(単体と結合)
- integration_test
- テストカバレッジの計測
- ビルド
- ビルドのアップロード
実行のタイミングについて
実行のタイミングは、パイプラインの目的によって変わります。
パイプラインとは、一連の手順のことで、具体的にCI/CDで実行するパイプラインは大きく分けると以下の二つになります。
- CIパイプライン
コードに問題がないか確認するパイプライン - デプロイ(リリース)パイプライン
デプロイ(リリース)するパイプライン
コードに問題がないか確認するパイプライン
こちらのパイプラインでは、リンターでの静的解析や、自動テストなどを行い、コードに問題がないかを確認します。
実行するタイミングは以下になります。
- PRが作成、更新されたとき
- PRのマージキューによるイベント
- 定期的な実行
※マージキューについては後述します。
※今回のサンプルでは定期的な実行は設定しません。
デプロイ(リリース)パイプライン
こちらのパイプラインでは、デプロイ(リリース)するのが目的です。
ブランチ戦略により異なるかもしれないですが、実行するタイミングは以下になるかと思います。
- PRをmainにマージする時
マージキューについて
マージキューを設定することで、PRの作成・更新時だけではなく、PRのマージ時にも自動テストなどのワークフローが実行できます。
複数人で開発をしていると、PRの作成・更新時のチェックでは問題ない場合でも、マージのタイミングによって、マージ後に問題が発生する場合があります。
それを防ぐため、マージキューという仕組みを使い、PRのマージが順番に行われるようにします。
マージキューの動作イメージ
※マージキューを設定し、GithubActionsでマージキューのマージ時にワークフローを実行する設定をしているとします。
- PR-AとPR-Bがほぼ同時にマージを実行する
- PR-AとPR-Bはマージキューに入れられる
-
マージ先のブランチにPR-Aをマージしたコード
でワークフローを実行する - 問題なければ、PR-Aをマージ
-
マージ先のブランチにPR-Bをマージしたコード
でワークフローを実行する
(この時点で、PR-Aはマージされているため、マージ先のブランチ + PR-A + PR-B
のコードでワークフローを実行していることになる) - 問題なければ、PR-Bをマージ
マージキューのマージ前のワークフローで自動テストなどを行い、問題があればマージは失敗するので、マージ前に、マージ後のコードでワークフローが成功するかを確認できる、かつ、複数のマージの競合も防げることになります。
マージキューの設定方法
別記事としたので、ご参照ください。
CIパイプラインを構築
CIパイプラインでは以下の項目を実行し、
- リンターでコードの品質を高める
- フォーマッターでコードのフォーマットチェック
- Unitテスト
- Widgetテストまたは、Goldenテスト
- integration_test
- テストカバレッジの計測
- ビルド(※一旦、スキップします)
構築方法
構築方法はとても簡単で、プロジェクトのルートに.github/workflows/ci.yaml
を作成し、内容を設定するだけとなります。
リンターからテストまでを実行
テストカバレッジの計測は、追加で設定が必要なので、一旦リンターからテストまでを設定します。
name: flutter_ci
on:
pull_request:
merge_group:
jobs:
CI:
name: CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create flutter environment
uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.4'
channel: 'stable'
cache: true
- name: Install packages
run: flutter pub get
- name: Check format
run: dart format lib --set-exit-if-changed -o none
- name: Analyze
run: flutter analyze
- name: Test
run: flutter test
テストカバレッジを計測
テストカバレッジのレポートは、flutter test --covoerage
というコマンドでファイルとして出力できます。
以下が参考になりました。
上記の記事では、スクリプトを使って、出力したファイルからカバレッジのデータを抽出していますが、Codecovという外部サービスと連携させて分析をすることもできます。
今回の記事では、Codecovと連携させる方法としています。
CodecovはFreeプランがあるので、そちらに登録し、以下を参照して設定してください。
(Option)カバレッジをフォルダごとに設定する
こちらの章はオプションなので不要な方は読み飛ばしてください
設定をカスタマイズせずに計測すると、コード全体のテストカバレッジを計測して、計算されます。
しかし、プロジェクトによっては以下のような形も考えられるのではないかと思います。
- Themeや、文字列定義などのファイルは対象外としたい
- Unit Testではカバレッジは80%にしたいが、Widget Testではもう少し低くても大丈夫
プロジェクトのルートに以下のような設定ファイルを追加します。
coverage:
status:
project:
default:
target: 80%
threshold: 5%
unit_tests:
target: 80%
threshold: 5%
widget_tests:
target: 70%
threshold: 5%
ignore:
- "lib/main.dart"
flag_management:
default_rules:
carryforward: true
statuses:
- type: project
target: auto
threshold: 5%
- type: patch
target: 70%
individual_flags:
- name: unit_tests
paths:
- lib/domain
carryforward: true
statuses:
- type: project
target: 80%
- type: patch
target: 100%
- name: widget_tests
paths:
- lib/presentations
carryforward: true
statuses:
- type: project
target: 70%
- type: patch
target: 20%
いくつか設定を解説します。
- ignore
カバレッジ計測の対象外となるファイルパスを定義します。
- "lib/main.dart"
という形で追加していくと対象のファイルは無視されます。 - individual_flags
Flagを設定することでFlagごとにカバレッジを計測できます。
今回は、unit_tests
とwidget_tests
というフラグを設定しています。
それぞれに目標値の設定が可能で、unit_tests
は80%、widget_tests
は70%としています。
この辺りの数値はプロジェクトや実際に運用してみて変えていく形になると思います。
テストを追加
テストコードは以下となります。
import "package:flutter_sample_ci/domain/sample_logic.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
final sut = SampleLogic();
test("Unit: getSampleText should return Hello, World!", () {
final result = sut.getSampleText();
expect(result, "Hello, World!");
});
test("Unit: getGreeting should return Hello, {name}!", () {
final result = sut.getGreeting("Flutter");
expect(result, "Hello, Flutter!");
});
}
import "package:flutter/material.dart";
import "package:flutter_sample_ci/presentations/my_app.dart";
import "package:flutter_test/flutter_test.dart";
void main() {
testWidgets("Widget: Counter increments smoke test",
(WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text("0"), findsOneWidget);
expect(find.text("1"), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text("0"), findsNothing);
expect(find.text("1"), findsOneWidget);
});
}
詳細はこちらを参照してください。
ポイントとしては、テストの名前の先頭にUnit
やWidget
という文字を付加していることです。
こうすることで、flutter test --name "Widget.*
と実行すると対象のテストのみ実行できます。
上記の設定ができたら、ワークフロー部分を修正します。
name: flutter_ci
on:
pull_request:
merge_group:
jobs:
CI:
name: CI
# 省略
- - name: Test
- run: flutter test
+ - name: Test
+ run: flutter test --name "Widget.*" --coverage --coverage-path="coverage/widget_tests/lcov.info"
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v4.0.1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ directory: coverage/widget_tests
+ flags: widget_tests
+ - name: Test
+ run: flutter test --name "Unit.*" --coverage --coverage-path="coverage/unit_tests/lcov.info"
+ - name: Upload coverage reports to Codecov
+ uses: codecov/codecov-action@v4.0.1
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ directory: coverage/unit_tests
+ flags: unit_tests
E2Eテスト
E2Eテストの実行はこちらの記事を参考に以下のようにしました。
name: flutter_ci
on:
pull_request:
merge_group:
jobs:
CI:
name: CI
# 省略
- name: Test
run: flutter test --name "Unit.*" --coverage --coverage-path="coverage/unit_tests/lcov.info"
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
directory: coverage/unit_tests
flags: unit_tests
+ - name: Run only on merge_group
+ if: github.event_name == 'merge_group'
+ #run: echo "This step runs only on merge_group events."
+ uses: reactivecircus/android-emulator-runner@v2
+ with:
+ api-level: 29
+ arch: x86_64
+ profile: Nexus 6
+ script: flutter test integration_test --verbose
サンプルのため、Androidのみ設定してますが、iOSも必要だと思います。
リリースパイプラインを構築
TODO(後日更新)