DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

ゼロ円から始めるクラウドの実機を使った自動テスト(iOS)

この記事はDeNA Advent Calendar 2018 その1の2日目の記事です

こんにちは。SWETの加瀬(@Kesin11)です。

2018年も残すところ一ヶ月弱となりましたが、今年一番のニュースと言えば何だったでしょうか? そう、Google IO 2018で発表されたFirebase Test LabのiOS版ですね!

Firebase Test LabはiOSとAndroidに対応した実際の実機デバイス上でテストを実行してくれるサービスです1
Android版は結構前から既に提供されており、Android Studioにも統合されているので使ったことがあるという方も結構いるのではないでしょうか?

一方でiOSに関してもFirebase Test Labと同様の実機デバイス上でテストを実行してくれるサービスはいくつかありますが、Firebase Test Labが将来的にiOSをサポートするかどうかという話はこれまで何もありませんでした。

そのような状況でしたので、Google IO 2018でFirebase Test LabのiOS版が発表されたとき個人的には大興奮でした。そして先日開催されたFirebase Summitでついに、βが取れて正式にリリースされたというアナウンスがありました🎉

iOS版ではテスト用のビルドをアップロードすると、Firebase Test LabにあるiOS実機でXCTest、XCUITestを実行してくれて、デバイスごとのテスト結果や動画などを確認できます。

前置きが長くなりましたが、今回はそんな待望のFirebase Test LabのiOS版について、以下の複数の利用方法についてそれぞれ解説していきます。

  • Bitriseから使う
  • gcloudコマンドから使う(ローカル)
  • gcloudコマンドから使う(CI環境)
  • Fastlaneプラグインから使う

なお、本記事の動作確認はこちらのリポジトリをベースにXCTestを追加し、XCUITestを少し修正したものを使用しています。
使用したXcodeのバージョンは10.1です。
https://github.com/browserstack/xcuitest-sample-browserstack

Bitriseから使う

Bitriseはモバイルアプリに特化したCI/CDサービスです。
もし既にBitriseをお使いであれば、現状ではこの方法が最も簡単、かつ無料の方法になります。

BitriseにはiOS Device Testingというステップが存在します。
これは名前の通りに実機デバイスでXCTest、XCUITestを実行し、テスト結果だけではなくてそのときのスクリーンショット、動画、デバイスログも保存してくれるものです。実は、その裏側はFirebase Test Labを使用していることが公式に解説されています。

Firebase Test Labは本来アカウントの認証が必要であったり、一定以上の使用には課金が必要です(認証と料金については後述)。 しかしiOS Device Testingを使用する場合にはFirebaseとのやりとりをBitriseがやってくれるため、アカウントの認証不要、さらに現在はβ版のためか無料で使うことができます。

使用可能なデバイスの一覧はiOS Device Testingのステップから確認できます。執筆時点での一覧はこのようになっていました。

f:id:swet-blog:20181130104530p:plain
Bitrise iOSデバイス一覧

さすがに最新のiPhone XSなどはないものの、iOS 12のiPhone Xは存在しています。 古めのデバイスも結構揃っており、最も古いものでiPhone 6が存在し、多少ですがiPadも種類が用意されています。さらにiOSのバージョンはiOS 9.0からそろっています。
さすがに全デバイス × パッチバージョンまで含めた全iOSバージョンの全てをカバーしているわけではありませんが、これだけのデバイス × iOSバージョンの実機を実際に手元で用意、管理するのはなかなか大変だと思います。これこそが、この手のサービスで最も嬉しいポイントではないでしょうか。

iOS Device Testingのさらに詳しい使い方やスクリーンショットは、Bitrise公式のドキュメントやブログが充実していますので、そちらを見てもらうと分かりやすいと思います。

Device testing for iOS
Introducing solid and snappy real device testing for iOS with Firebase (beta)

gcloudコマンドから使う(ローカル)

Bitriseを使っていない場合、gcloudコマンドから直接Firebase Test Labを使う方法になります。

https://firebase.google.com/docs/test-lab/ios/command-line?hl=en

Firebase Test Labを使うにはアカウントの認証と、紐付けるプロジェクトの選択が必要になります。
まずはドキュメントに従ってgcloud auth logingcloud config set project PROJECT_IDを実行してください。PROJECT_IDはfirebaseのプロジェクトIDです。もしまだfirebaseのプロジェクトを作成していない場合、先にfirebaseのプロジェクトを作成しておいてください2

gcloudコマンドのセットアップができたら、まずは使用できるデバイスの一覧を見てみましょう。

$ gcloud firebase test ios models list
┌──────────────┬───────────────────────┬─────────────────────┬─────────┐
│   MODEL_ID   │          NAME         │    OS_VERSION_IDS   │   TAGS  │
├──────────────┼───────────────────────┼─────────────────────┼─────────┤
│ ipad5        │ iPad (5th generation) │ 11.2,12.0           │         │
│ ipadmini4    │ iPad mini 4           │ 11.2,12.0           │         │
│ ipadpro_105  │ iPad Pro (10.5-inch)  │ 11.2                │         │
│ iphone6      │ iPhone 6              │ 11.4                │         │
│ iphone6s     │ iPhone 6s             │ 10.3,11.2,11.4,12.0 │         │
│ iphone6splus │ iPhone 6s Plus        │ 9.0,9.1             │         │
│ iphone7      │ iPhone 7              │ 11.2,11.4,12.0      │         │
│ iphone7plus  │ iPhone 7 Plus         │ 11.2,11.4,12.0      │         │
│ iphone8      │ iPhone 8              │ 11.2,11.4,12.0      │ default │
│ iphone8plus  │ iPhone 8 Plus         │ 11.2,11.4,12.0      │         │
│ iphonese     │ iPhone SE             │ 11.2,11.4,12.0      │         │
│ iphonex      │ iPhone X              │ 11.2,11.4,12.0      │         │
└──────────────┴───────────────────────┴─────────────────────┴─────────┘

先述のBitriseと同じリストが表示されましたね。では次にテスト用のビルドを作成します。

ドキュメントにあるようにxcodebuildでビルドします。 アプリ名とスキームがMyAppで、ビルド結果の出力先をfirebase_testとした場合には以下のようなコマンドになります。

$ xcodebuild -project MyApp.xcodeproj \
  -scheme MyApp \
  -derivedDataPath firebase_test \
  -sdk iphoneos build-for-testing

さらにFirebase Test Labでテストを実行するには、テスト用のビルド成果物とxctestrunファイルを1つのzipに圧縮する必要があります。

$ cd firebase_test/Build/Products
$ zip -r MyTests.zip Debug-iphoneos MyApp_iphoneos12.1-arm64e.xctestrun

あるいはFastlaneを導入済みならばscanbuild_for_testingオプションを使ってテスト用ビルドを生成しても大丈夫です。

scan(
  scheme: 'MyApp',
  clean: true,
  skip_detect_devices: true,
  build_for_testing: true,
  sdk: 'iphoneos',
  should_zip_build_products: true
)

テスト用のビルドができたらいよいよテストを実行してみましょう。

$ gcloud firebase test ios run --test MyTests.zip \
  --device model=iphonex,version=12.0,locale=ja_JP \
  --device model=iphone8,version=11.4 \
  --device model=iphone6splus,version=9.1 \
  --device model=iphone6s,version=10.3,locale=ja_JP,orientation=landscape \
  --device model=ipad5,version=12.0 \
  --xcode-version=10.1

テストを実行したいデバイスの設定を--deviceの引数に渡します。modelとversionには先ほどのモデル一覧で表示されたMODEL_IDとOS_VERSION_IDSを指定します。必要であればlocaleとorientationも指定可能で、指定しなかった場合のデフォルトはenとportraitとなります。
指定可能なlocaleについてはgcloud firebase test ios locales listで一覧の確認が可能です。

--xcode_versionはFirebase Test Lab側でテストを実行するXcodeのバージョンのようです。執筆時点ではXcode 10.1でビルドした場合に--xcode_versionの指定無しでも動くことを確認しましたが、古いXcodeを使用している場合や、新しいXcodeが出た直後などにはバージョンを指定しておいた方がいいかもしれません。

特にエラーなくコマンドが実行できたら、まだテスト実行中の段階からFirebase Test Labのページで結果を見ることができます。
実行が完了するとこのように複数のデバイスでテストケースごとのテスト結果、スクリーンショットや動画を確認できます。

f:id:swet-blog:20181130104903p:plain
デバイス毎の実行結果
f:id:swet-blog:20181130104911p:plain
テストケースの結果

gcloudコマンドから使う(CI環境)

ローカルの環境から実行できることは確認できましたが、アプリのビルドやテストはローカルとは別のCI環境で実行していると思いますので、継続的にテストを実行させるにはCI環境からFirebase Test Labを実行をできるようにしておくのがよいでしょう。

しかし、先述のgcloud firebase test iosを実行するにはアカウントの認証が必要です。そして認証にはブラウザでログインする必要があるので通常の方法ではCI環境で認証することはできず、テストを実行できません。

この問題はサービスアカウントを使って認証することで解決できます。この方法に関してのドキュメントはAndroid版の方に一応存在するのですが、あまり分かりやすいとは言えないため解説します。

サービスアカウントによる認証

まずはGCPのコンソールからサービスアカウントを作成します。
https://console.cloud.google.com/iam-admin/serviceaccounts/

サービスアカウントの権限には「プロジェクトの編集者」の権限を付ける必要があります。

f:id:swet-blog:20181130105326p:plain

権限の設定が完了したら最後にjson形式で鍵を生成してください。

次にGoogle Developers Console API Library pageからGoogle Cloud Testing APICloud Tool Results APIを検索し、両方とも有効化してください3

jsonの鍵を用意できたらgcloudコマンドがサービスアカウントを使うように認証させます。

$ gcloud auth activate-service-account --key-file YOUR_SERVICE_ACCOUNT_KEY.json

gcloud auth listを実行してサービスアカウントにACTIVEが付いていればOKです。

この状態で再度gcloud firebase test ios runを実行して、まずはローカルの環境で実行できることを確認しておきましょう。

(サービスアカウントの認証がうまくいかない場合)

自分も実際に遭遇したのですが、上記の手順通りに作成したサービスアカウントでもなぜかFirebase Test Labを実行するときにエラーとなってしまう場合があるようです。
https://stackoverflow.com/questions/48327027/cant-run-firebase-test-lab-tests-using-gcloud-and-service-account-403-does-no?rq=1

上記のベストアンサーではない方の回答に、Firebaseのサービスアカウントの鍵を使うと動作したという回答がありました。Firebaseのサービスアカウントの鍵は以下のurlからアクセスできるページで作成できます。
https://console.firebase.google.com/u/0/project/FIREBASE_PROJECT_ID/settings/serviceaccounts/adminsdk
(FIREBASE_PROJECT_IDのところは自分のプロジェクトidに書き換えてアクセスしてください)

自分が試したところ、たしかに動作することを確認しました。詳しい理由は不明ですが、自分と同じようにGCPで作成したサービスアカウントではどうしてもエラーになってしまう場合に試してみるといいかもしれません。

CIサービスのビルドフローの設定

サービスアカウントの認証ができたところで、次はCIサービスのビルドフローに組み込んでみましょう。

お使いのCIサービスでgcloudコマンドを使えるようにし、先ほどと同様にサービスアカウントで認証をしてからテストを実行するフローを構築してください。少し注意が必要なのは、サービスアカウントの鍵のjsonです。 チームの体制にもよりますが、一般的にはリポジトリにコミットするべきではないでしょう。しかし、使用するCIの環境によっては秘密にしておきたいファイルをリポジトリとは別に登録するという方法ができない場合もあるかと思います。
そのような場合は、CircleCIのドキュメントにあるようにjsonの中身を環境変数として登録しておき、ビルドフローの中で再度jsonとして書き出すという方法が使えます。
Language Guide: Android - CircleCI

実際のサンプルとしてBitriseでgcloudを使ってサービスアカウントの認証、テストを実行する方法を紹介します。

まず、サービスアカウントの鍵であるjsonの中身をcatとpbcopyなどを使って丸ごとコピーして環境変数にセットしておきます。以下のコードでは$SERVICE_ACCOUNT_KEYで参照しています。

BitriseのScriptステップはシェルスクリプトをそのまま実行できるため、ここでインストールから認証まで行います。

#!/usr/bin/env bash

# Bitriseはデフォルトでgcloudがインストールされていないため、インストールから行う
curl https://sdk.cloud.google.com | bash > /dev/null 2>&1

# gcloudコマンドが使えるようにPATHを通す
source "${HOME}/google-cloud-sdk/path.bash.inc"
gcloud version

# サービスアカウントで認証
## 環境変数からjsonに復元する
echo ${SERVICE_ACCOUNT_KEY} > "${HOME}/gcloud-service-key.json"
gcloud auth activate-service-account --key-file "${HOME}/gcloud-service-key.json"
gcloud auth list

# プロジェクトidを設定
gcloud config set project ${GCLOUD_PROJECT}

後はテスト用のビルドを作成し、gcloud firebase test ios runを実行するだけです。テスト用のビルドについては説明済みですので、今回は省略します。

#!/usr/bin/env bash

# BitriseのScriptステップはPATHが引き継がれない(?)ようなので再度PATHを通す
source "${HOME}/google-cloud-sdk/path.bash.inc"

# Xcode Build for testing for iOSのステップでビルドすると$BITRISE_TEST_BUNDLE_ZIP_PATHというパスにzipされた成果物が保存されている
gcloud firebase test ios run --test "${BITRISE_TEST_BUNDLE_ZIP_PATH}" \
  --device model=iphone8,version=11.4 \
  --device model=iphonex,version=12.0,locale=ja_JP \
  --device model=iphone6splus,version=9.1 \
  --device model=ipad5,version=12.0 \
  --device model=iphone6s,version=10.3,locale=ja_JP,orientation=landscape

上記のサンプルには主にgcloudコマンドをセットアップするBitrise特有のコードも含まれてしまっていますが、サービスアカウントで認証してテストを実行する部分は他のCI環境でも同様に動作すると思います。

もしもより詳しいドキュメントを探す場合は、前例の多いAndroid版のFirebase Test Lab + CircleCIの組み合わせの記事を探してみるのがよいでしょう。

Fastlaneプラグインから使う

もしFastlaneを導入済みであれば、Firebase Test Labを実行するFastlaneプラグインを使用するという選択肢もあります。
fastlane/fastlane-plugin-firebase_test_lab

使い方はREADMEに書いてあるように、scanでテスト用のビルドを行ってからfirebase_test_lab_ios_xctestでテストを実行します。ただし注意点として、READMEのサンプルをそのまま使用した場合はgcloudのデフォルトの認証情報が使われます。つまりgcloudのコマンドから実行したときと同様に、CI環境では動きません。
oauth_key_file_pathというオプションにサービスアカウントの鍵のパスを指定することで、サービスアカウントで認証を行うことができます。

# fastlane-plugin-firebase_test_labのREADMEから転載
scan(
  scheme: 'YourApp',                  # XCTest scheme
  clean: true,                        # Recommended: This would ensure the build would not include unnecessary files
  skip_detect_devices: true,          # Required
  build_for_testing: true,            # Required
  sdk: 'iphoneos',                    # Required
  should_zip_build_products: true     # Must be true to set the correct format for Firebase Test Lab
)
firebase_test_lab_ios_xctest(
  oauth_key_file_path: '/your/servie_account/path.json', # このオプションにサービスアカウントの鍵のjsonのパスを指定する

  gcp_project: 'your-google-project', # Your Google Cloud project name
  devices: [                          # Device(s) to run tests on
    {
      ios_model_id: 'iphonex',        # Device model ID, see gcloud command above
      ios_version_id: '11.2',         # iOS version ID, see gcloud command above
      locale: 'en_US',                # Optional: default to en_US if not set
      orientation: 'portrait'         # Optional: default to portrait if not set
    }
  ]
)

ただし注意点として、FastlaneのプラグインはコミュニティによってメンテされているためXcodeのバージョンが上がった場合などに動かなくなる可能性があります。
実際に、Xcode 10.1でxctestrunの形式が微妙に変わったことで動かなくなってしまったバグの修正PRがついこの前マージされたようです。 このように何かがバージョンアップされた場合に壊れてしまうリスクがあるということは覚えておいた方がいいでしょう。

料金

最後は気になるお値段の方ですね。

まず、Bitriseで実行した場合は現在のところ無料です🎉
おそらく現在はまだβであることが無料の理由だと思いますので、今後どうなるかはBitrise Blogなどをウォッチしておくといいでしょう。

Bitriseを使わずに直にFirebase Test Labを使用する場合は無料プランと、従量課金プランがあります。

まず、無料のSpark Planの場合はテスト5回/日のようです。チームの規模が大きい場合には全てのpull-reqごとに実行するような使い方は難しいと思いますので、テストを実行するブランチを絞ったり、深夜ビルドでのみ実行するといった工夫が必要かもしれません。

従量課金制のBlaze Planにした場合、回数の上限はなくなり$5/デバイス/時間となります。これの意味するところは、テストを実行したデバイス全ての稼働時間の合計が1時間につき$5という意味です。 具体的な例としては、1回の実行に12分かかるテストを5台のデバイスで実行した場合、合計で1時間=$5と計算されるということになります。

ちなみに、料金はVirtual DeviceとPhysical Deviceに分かれていますがVirtual Deviceは今のところAndroidだけ存在しており、iOSの場合はPhysical Deviceのみとなります。

上記の料金の計算は執筆時点(2018年12月)のドキュメントを参考にしたものであり、必ずしも正しいことを保証するものではありません。
また、Firebaseの料金は今後変化する可能性がありますので詳しくはFirebaseの料金のページを参照するようにしてください。

まとめ

Bitrise、gcloudコマンド、FastlaneのそれぞれでFirebase Test Labによる実機テストを行う方法の解説をしました。Firebase Test Labはとても簡単に使えて、無料プランもあるのでこの手のサービスの中ではかなり試しやすいサービスだと思います。

ビルドのたびに様々なiOSデバイス、iOSバージョンの実機で自動テストを実行することが理想ですが、人手を介さずにそれを全自動で行ってくれるシステムを構築・運用することはとても難しいです。 そのようなシステム構築やデバイスの管理をする必要がないFirebase Test Labを使って、ゼロ円から実機の自動テストを始めてみませんか?


  1. Android版では実機だけではなくエミュレータを使用してテストを実行することも可能です。

  2. 実はGCPのプロジェクトIDでも実行することは可能ですが、テスト結果画面を見るにはFirebaseのプロジェクトページにアクセスする必要があるため、どのみちGCPのプロジェクトに紐づけたFirebaseのプロジェクトを作成する必要があります。

  3. ここで有効化しなかった場合でもサービスアカウントで認証後にTest Labを実行するとAPI [toolresults.googleapis.com] not enabled on project [****]. Would you like to enable and retry (this will take a few minutes)? (y/N)? というように有効化するか尋ねられるので、そこでyesとしても大丈夫なようです。

DeNA TechCon2019でテストによる開発プロセス改善の実例について発表します

2019年2月6日(水)に開催予定のDeNA TechCon 2019でSWETのKuniwakが、テストによる開発プロセス改善の実例に関して紹介いたします。

タイトル

あるSWETエンジニアの開発プロセス改善最前線

登壇者

f:id:swet-blog:20181128132254j:plain

@orga_chem(Kuniwak)こと国分佑樹

2018年からSWETチームで働いているエンジニアです。専門はユニットテストやプロセス改善による開発の安定化と高速化。最近の活動では、iOSDC Japan 2018ベストトーク賞2位、Qiita Advent Calendar 2017最多いいね数賞など。オープンソース活動も活発にしており、静的検査ツールや効率的なテストを支援するためのライブラリ等を開発しています。

登壇内容

私の所属するSWETという組織の目指すものは「テストを楽しく、賢く、価値を届けられるように」であり、私たちは日々これを達成すべく働いています。このような組織に所属する私が取り組んでいるものは、テストをうまく使って開発速度の安定化や向上、より保守性の高い設計を達成することです。この発表では、私が取り組んできたテストによる開発プロセスの改善の実例をお話しします。

この発表では、以下の5つの事例を紹介する予定です:

  • 現場の開発プロセスの分析
  • 開発プロセスのコンサルティング
  • デバッグを助けるモジュールの開発
  • テスターを助けるライブラリの開発
  • Jenkinsクラスタの安定稼働のための移行プロジェクト

DeNA TechCon2019について

DeNAでは、ゲームやエンタメだけでなく、Eコマース、ソーシャルLIVE、ヘルスケア、オートモーティブ、そして横浜DeNAベイスターズのようなスポーツなど、様々な事業を行っています。また、技術の力で事業の未来をリードするため、様々なチャレンジも行っています。

DeNA TechConはこれらのチャレンジの中から、技術的に特に面白いものを厳選して皆さんにお披露目するお祭りです。このDeNA TechConの次回開催内容が決定したのでお知らせします。

今年のテーマは「SHIFT UP」です。

『面白かったー!』と思っていただく工夫を凝らしてお待ちしてますので、みなさまぜひお越しください!

エントリーフォームはこちら https://techcon2019-dena.peatix.com/

DeNA TechConの最新情報は、公式Twitterアカウント(@DeNAxTech)にて確認いただけます。ぜひフォローをお願いします!

CI/CD Test Night 2days開催レポート

はじめまして、SWETの井口(@hisa9chi)です。

2018年11月7, 8日の2日間で、SWET主催によりCI/CD Test Night #1,#2 を開催させていただきました。本勉強会では近年注目されているCIサービスであるBitriseからCTO:Viktor Benei氏(@ViktorBenei)とVP of Growth:Hendrik Haandrikman氏(@HHaandr)のお二方に来日登壇していただきました。

Bitrise社のお二方が来日していただけることとなった経緯と、当日の発表内容に触れつつ、初開催である本勉強会の開催レポートとして投稿させていただきます。

f:id:swet-blog:20181115161607j:plain

最初に

Test Nightとは

Test Night とは、SWETが主催する主にテストに関係することについて語り合うことを目的とした勉強会です。

2016年11月からiOSに特化した「iOS Test Night」を、2017年9月からAndroidに特化した「Android Test Night」をそれぞれ定期的に開催しています。

これまでに多数の発表がおこなわれ、テストに関する知見を共有し・活発な議論がおこなわれる勉強会となっています。

CI/CD Test Nightとは

CI/CDはテストにも大きく関係する技術であるということから、Test Nightの管理するイベントとして新たにCI/CDに特化した「CI/CD Test Night」の開催に至りました。

CI/CDに関する知識、環境やそれら利用していく上でのツラミなどを広く共有することを目的としております。

弊社とBitrise

Bitriseの利用

2017年11月頃よりSWETチーム内にて Bitrise の利用調査を開始しました。同年12月頃には社内で実用性を検証するため、サービス開発している部門の協力でトライアル導入を行い検証を続けておりました。その結果、かなり有効活用ができると判断したため、翌年の2018年1月頃に正式導入となりました。これら導入の経緯は勉強会の1日目に、「Bitriseの社内提供へ」と題して、私から発表させていただきました。

Bitriseとのコラボレーション

弊社とBitrise社との交流の始まりは、iOSDCにて弊社が実施していたアンケートをツイートしたことが始まりでした。

このアンケート内で「CI環境はどこを使っているか」という設問にて、弊社のサービス開発で主に利用しているBitrise, CircleCI等を記載していました。上記のツイートを見ていただければわかりますが、アンケートではBitrise利用者の多さが目立っています。弊社が調査検証を始めていた頃から、iOS開発界隈では注目度がかなり高かったため、その注目度が結果にも表れたのだと思います。

そして、このツイートがBitrise社のHendrik氏の目に止まり、弊社の技術企画宛に直接メッセージをいただきました。このことをきっかけに、Bitrise社を本勉強会に招待するという形で開催が決定いたしました。さらには、せっかくハンガリーよりはるばる来日していただけるので、以下の目的から1日ではなく2日開催としました。

  • Bitrise社に多く話をしていただける場としたい
  • Bitrise社と日本の開発者がコミュニケーションできる場としたい
  • 登壇や聴講でより多くの方に参加していただきたい
  • 自分たちのCI/CD環境について語りあえる場としたい

CI/CD Test Night

ここからは当日の発表内容についてレポートしていきます。

day 1

keynote: Bitrise: Workflows, Integrations and More

1日目の基調講演では、最初にHendrik氏よりBitriseのこれまで歴史と現在をご紹介いただきました。

その中で特に驚いたのは、 Bitriseの市場の中で、日本は世界第3位の規模であるということです。そして、2019年の初期には第2位の規模になる見込みだということです。加えて、Bitriseに関する会話の多くは日本で交わされているとのことです。

その後、Viktor氏より動画を交えながら、プロジェクトの追加からビルド実行までの基本的な設定や、Workflowを構成する代表的なStepsについても説明していただけました。そして、ここでも驚くべきことがあったのですが、Workflowにて利用可能なStepsの約50%はコミュニティによって作成されたということです。

最後に、よく寄せられる質問として以下に関してご説明もいただきました。

  • iOS Code Signing
  • Locating artifacts
  • Attaching artifacts to the builds
  • Reliable Android Espresso/UI tests

内容の詳細は発表資料が公開されておりますのでそちらをご確認いただければと思います。

公募セッション

1日目はLT(5名)を含め7名の方にご登壇いただきました。

タイトル 発表者
Bitriseの社内提供へ Hisashi Iguchi
bitcodeを有効にしたアプリでもdSYMのアップロードを自動化する qmihara
ゼロからはじめない方がいいiOSアプリのビルドシステム構築 dotrikun
Track Everything - すべてをGitHub/Bitriseで追跡可能にする kishikawa katsumi
Workflow、どう組んでいますか? ホリエ
サイボウズのCI/CD事情 - JenkinsおじさんはCircleCIおじさんにしんかした! miyajan
Wantedly Peopleのアプリのリリースワークフロー Ryo Sakaguchi

それぞれ、自社でCI/CD環境を利用した取り組みや、BitirseだけでなくJenkinsやCircleCIを活用されているお話も聞けました。BitriseとCircleCIの両方を導入している弊社としても多くの事例が聞けてよかったと思います。

中でも個人的に興味深かったのは、kishikawa氏とmiyajan氏の発表でした。

kishikawa氏の発表ではテスト、ビルド(手元でのビルド以外)、デプロイをBitriseにて自動化されているということと、それら自動化されたイベントの証跡を辿れるようにしているということでした。CI/CDで自動化するというだけでなく、のちに振り返れるように証跡を辿れることも重要だと思った内容でした。

miyajan氏の発表の中でJenkins おじさん、CircleCI おじさん と出てきた時にあるあるとだなと思っていました。また、その発表の中でCircleCI 2.1の機能に関しての紹介されていました。弊社はCircleCI Enterpriseを利用しており、Enteprise版にはリリースされていないので利用できませんが、リリースされて早く社内でも利用できるようになるいいなと切望しながら聞いていました。

発表の資料はこちらのイベントページに随時公開されていきますので、そちらをご確認いただければと思います。

day 2

keynote: Advances Bitrise and Roadmap Features

2日目の最初も、 Bitrise社のViktor氏とHendrik氏に登壇いただきました。2日目は初日とは異なり、少し突っ込んだお話をしていただけました。Bitrise CLIに加えbitrise.ymlのdiff、Dangerを用いたレビューに関して、Firebase Test Labとそのテストランナーである Flank を利用する事例を紹介していただけました。Flankに関しては私も初めて知り、多くのデバイスを並列して利用でき、テストの総実行時間の短縮に繋がるので利用する効果は高いと思いました。

その後は、これから先のロードマップを紹介していただきました。今後以下の数多くの施策があるようです。

  • Dashboardのリニューアル
  • Firebase Testlab iOSへの対応
  • 組織管理の改善
  • Open Source support
  • SAMLへの対応
  • 新しいビルドシステム
  • Enterprise Support
  • Premium Support

中でも驚いたことは、Enterprise Supportに関して、データセンターの東京設置が考えられているということです。さらに、日本語サポートを可能とする体制も整えることを視野に入れられているようです。

加えて、来日が決まってから、Bitriseの基本20ドキュメントを日本語化する作業に取り掛かったようです。この作業は、多くのコミュニティが賛同して貢献してくれたおかげで、迅速に完了したとのことでした。そして、本勉強会開催の頃には約50ドキュメントが日本語化されているようです。これらドキュメントはGitHub上で管理されているようです。

最後のQAでは、日本のユーザがどのようにBitriseを利用しているか教えて欲しいということを聴講者に伝えていました。その上で、ユーザの方々が欲しい機能であれば、今後ロードマップに追加・修正するといったことも考えているということでした。そのような意向を伝えられたこともあり、会場からはいくつもの改善要望が上がり、Bitrise社と日本の開発者をつなぐことが出来たのではと思いました。

内容の詳細は発表資料が公開されておりますのでそちらをご確認いただければと思います。

公募セッション

2日目はLTも含め6名の方にご登壇いただきました。

タイトル 発表者
CI/CD Test on ReRep Spring_MT
私とBitriseとCircleCI - 私がAndroid CI/CDを移行して得られたもの nemoto_tadashi
アプリの検証がどのくらい行われているか調べてみた henteko
React NativeアプリをBitriseでCDする れこ
社内ライブラリの更新を自動化する話 Ryo Sakaguchi
CI/CD専用モニタと心理的安全性 nakasho

1日目同様に、それぞれのCI/CDを活用した事例を紹介していただきました。

Spring_MT氏とnemote氏の発表では用途に応じて、BitriseとCircleCIを使い分けていることです。開発者がその時に手がけているプロダクトによってCI/CDサービスを選択でき、導入が可能である状況を準備することも重要だと改めて思いました。

また、個人的に面白いなと思ったのはnakasho氏が話されていたCI/CD専用のモニタを活用するということです。こういったものが1つあるだけで、開発者同士で新たなコミュニケーションが生まれ、より良いチーム形成にもつながるのではと思いました。

発表の資料はこちらのイベントページに随時公開されていきますので、そちらをご確認いただければと思います。

終わりに

今回の勉強会では、非常に多くの方に参加していただけ、皆様のCI/CDに対する興味の高さが実感できた会でもありました。当日の登壇内容は動画にて配信しております。参加できなかった方、あるいは参加された方も、当時の雰囲気や発表を再度確認する目的でご覧いただけると嬉しい限りです。

  • CI/CD Test Night #1 動画
  • CI/CD Test Night #2 動画

今回はBitrise社の方を招いて基本的な利用方法や、今後のロードマップに関してお話していただきました。その話の中で、日本市場へ対する投資を惜しまない姿勢がすごく伝わってきました。日本の方をブダペスト本社へ数週間インターンとして招くことや、日本でEnterprise提供に向けデータセンター設置する可能性、サポートのための日本人採用など驚くものばかりでした。今後も定期的に来日するとのことでしたので、機会があれば今回のようにコラボレーションできればなと思います。

CI/CD Test Nightは今回限りの開催ではなく、定期的に開催していこうと思います。そして今回のように、CI/CDサービスを提供している企業様を招いて、サービス提供側のお話も聞くことができるより良い会の運営を目指していきたいと思います。

最後とはなりましたが、今回のイベントを大盛況で開催できたのも、Bitrise社のご協力があったからこそだと思います。遠いハンガリーより来日していただいた上に、参加者のために数多くのノベルティを持参いただき、2日間にわたりKeynoteでご登壇いただいたViktor Benei氏とHendrik Haandrikman氏の両名には感謝しきれません。さらに、本勉強会の当日の様子などをBitrise社のBlogにも投稿いただきありがとうございます。

We truly appreciate Bitrise. Without their cooperation, our event would not have been so successful!! Thank you for everything, Bitrise ,Hendrik, and Viktor.

f:id:swet-blog:20181115161737j:plain

おまけ

参加者向けにBitriseより持参いただいたノベルティ各種です。

f:id:swet-blog:20181115161905j:plain:w135 f:id:swet-blog:20181115161735j:plain:w135 f:id:swet-blog:20181115161730j:plain:w135 f:id:swet-blog:20181115162812j:plain:w135

本日(11/5)販売開始:書籍「Androidテスト全書」のUIテスト部分を執筆しました

こんにちは。SWETの外山(@sumio_tym)です。 本日、書籍「Androidテスト全書」の一般販売が開始されました! 本書のUIテスト部分(第4〜6章)は、SWETの平田(@tarappo)と私が執筆しています。

f:id:swet-blog:20181029165224j:plain

本書の一般販売を記念して、本エントリーでは本書全体の概要と想定読者を紹介してから、 それぞれの担当著者がUIテスト各章の「見どころ」を解説します。

本書の概要

本書の目次は次のとおりです。

  • 第1章:テスト入門
  • 第2章:ユニットテスト実践入門
  • 第3章:ユニットテスト応用編
  • 第4章:UIテスト(概要編)
  • 第5章:UIテスト(Espresso編)
  • 第6章:UIテスト(Appium編)
  • 第7章:JUnit 5
  • 第8章:CI/CD

ユニットテストやUIテストはもちろん、 最新のテストフレームワークであるJUnit 5や、 テストの運用や開発プロセスの効率化に欠かせないCI/CDまでしっかり解説されています。

このようにAndroidのテスト(正確には自動テスト)すべてをカバーしている本書ですが、 公式サイトのチュートリアルのレベルにとどまることなく、実践的な内容まで踏み込んで解説しています。

また、ネット上に散らばっている古い情報や間違った情報に振り回されることなく、 最新の正しい情報を学べるというのも本書の大きな利点です。

本書の対象読者

このように本書の内容はとても充実していますが、 Androidのアプリ開発経験があれば自動テスト未経験でも読み進められるように意識して書かれています。

本書の「まえがき」には対象読者として次のように書かれています。 Androidの自動テストに関心のある方であれば、自動テスト経験にかかわらずおすすめの一冊になっています。

  • テストを書いた方がいいと思いつつ書けていない方
  • テストを書いているが、もっといい書き方があると思っている方
  • さらに発展的な内容を求める方
  • 安定したアプリを継続的にリリースしたいすべてのAndroidエンジニア

第4章「UIテスト(概要編)」の見どころ(執筆:外山)

4章ではUIテストにありがちな誤解を無くすべく、次の内容を盛り込みました。

  • UIテストを書きはじめる前に検討した方が良いこと
  • EspressoとAppiumの比較
  • テストツールの種類に依存しない、UIテストを長く運用し続けるためのポイント

実は、本書の当初の構想ではUIテストの解説はEspressoとAppiumのみの予定でした。 しかし、具体的な章立ての検討に入った時点で、当初の構想では次のような懸念を払拭し切れないことに気付きました。

  • いきなりUIテストツールの解説が始まると、読者が常にUIテストを書くべきだと誤解しないだろうか?
  • EspressoとAppiumの特徴がわからないと、読者がどちらを採用すれば良いか分からないのではないか?

それらに対する解決策として執筆したのがこの第4章です。

「UIテストを書き始めたけど辛くなってやめてしまった」という悲劇が少しでも減ることを願っています。

第5章「UIテスト(Espresso編)」の見どころ(執筆:外山)

5章は、Espressoの入門者が本書から学ぶことで、実用レベルのUIテストを書けるようになることを目指して執筆しました。

Espresso Test Recorderの使い方やEspresso APIの概要といった基本的な部分をきちんと押さえつつ、 実用レベルでは必ず必要となる次の点を丁寧に解説しています。

  • Page Objectデザインパターンを実践して長く使われ続けるテストにする方法
  • (難しいIdlingResourceだけに頼らない)画面更新の完了を待つためのテクニック

また、 テストを書いていてよく直面する「これってどうすればテスト書けるんだろう?」を解決すべく、 良くあるシーン別の解決方法も説明しています。

まさに、Androidアプリ開発を一通り学んだエンジニアがEspressoを自習できる内容に仕上がったと思っています。 本章をきっかけに、Espressoを実用レベルで使えるエンジニアが増えることを願っています。

第6章「UIテスト(Appium編)」の見どころ(執筆:平田)

この6章では他の章とは異なり、Android特有の話というわけではなく、AppiumというUIテストツールについて書いています。

本章では、このAppiumを使ってUIテストを実装する方法を書いています。 また、本書のために弊社のプロダクトに対して実際にUIテストを1から導入した話を載せています。

UIテストについての日本語の情報は少しずつ増えているように思いますが、まだまだ少ない状況だと思います。 特に実際にUIテストを導入した実績が書かれた話はあまり見たことがありません。

また、5章ではEspressoにふれています。 EspressoとAppiumの2つについて紹介し、それぞれのことを1冊の本で紹介するというのはなかなかないと思います。 これらの情報が、少しでも皆様の知見になればと思います。

終わりに

SWETメンバーが執筆に参加した「Androidテスト全書」について紹介しました。

Androidアプリ開発を一通り学んだエンジニアがテストについて学ぶ際に「次に読む一冊」として自信を持っておすすめできます! このエントリーを読んで少しでも興味を持たれた方は、 是非本書の公式サイトをチェックしてみてください!

また、11/30(金)には 「Android Test Night #5 - Androidテスト全書の回 -」 と題したイベントの開催を予定しています。 本書の筆者、編集者ともに登壇予定ですので合わせてご参加ください!

WebdriverIOでAppiumを使う勘所

こんにちは。SWETの外山(@sumio_tym)です。 SWETの一員になって約9か月、Appiumを使ってAndroidアプリのUIテスト自動化を行いつつ、 DroidKaigi 2018に登壇したり、 書籍「Androidテスト全書」を執筆したりしていました1

先日(2018年9月28日)に電子版が正式リリースされた「Androidテスト全書」の宣伝をしたいのもやまやまなのですが、 今回はAppiumを使ったAndroidのUIテスト自動化業務で得られたノウハウを紹介します。

Appiumを使ったUIテスト自動化経験があっても、使っているプログラミング言語は組織やプロジェクトによって様々だと思います。 本エントリーでは、Appiumの経験はあるもののJavaScriptでテストを書くのは初めての方に向けて、 クライアントライブラリにWebdriverIOを採用するときのノウハウを次の3点に分けて説明します。

  • セットアップするときのポイント
  • テストコードを書くときのポイント
  • テスト失敗時に解析しやすくする工夫

Appium経験者に向けたエントリーであるため、Appiumの詳しい解説はしていません。 Appiumについて詳しく知りたい方は公式ドキュメントを参照してください。

また、ノウハウの一部はAndroidでしか通用しないものもありますのでご注意ください。

WebdriverIOとは

Appium向けのクライアントライブラリのうち、JavaScriptに対応しているものは次の2つです。 どちらもJavaScript(Node.js)によるWebDriver実装です。

今回取り上げるWebdriverIOで特徴的なものは次の2点です。

  • 同梱されているCLIベースのテストランナーwdio
  • WebdriverIOサービスプラグインwdio-appium-service

wdioを使うと、async/awaitを使わずともテストコードを同期的に書くことができます。 JavaScriptに付きものの非同期処理から解放されてテストコードを書けるのは、思いの外便利です。

wdio-appium-serviceはWebdriverIOとAppiumを協調動作させるためのプラグインです。 このプラグインを使えばテスト実行・終了と合わせて、自動的にAppiumを起動・終了してくれます。

以降で説明するようにセットアップすれば、両者の機能をフル活用してテストを書き始めることができます。

なお、今回は次のバージョンで動作を確認しました。

  • Node.js: 10.9.0
  • WebdriverIO: 4.3.12
  • Appium: 1.9.0
  • wdio-appium-service: 0.2.3

セットアップするときのポイント

インストールと設定ファイルの雛形作成

Node.jsは既にインストールされているものとします。 テストコードを書くための空ディレクトリを作成し、 以下のようにwebdriverioパッケージをインストールします。

$ npm init
$ npm install --save-dev webdriverio

次に、以下のコマンドを実行します。 このコマンドは対話形式でWebdriverIOの設定ファイルを作成するものです。

$ npx wdio config

いくつか質問されますが、以下の質問以外は全てデフォルトのままEnterを押して先に進めてください。

  • Which reporter do you want to use?: テスト結果レポートをどのように表示するか選択します。
    こだわりがなければspecjunitを選択しておくのが無難です。
  • Do you want to add a service to your test setup?: appiumを選択します。 ここでappiumを選択すると、テスト実行の度にAppiumが自動的に起動するようになります。
  • Level of logging verbosity: 原因解析に有用ですのでverboseにするのがお勧めです。

全ての質問に回答するとWebdriverIOの設定ファイルwdio.conf.jsの雛形が生成されます。 あわせて、関連パッケージが自動的にインストールされます。

最後にAppium本体とアサーションライブラリ(ここではchai)をインストールします。

$ npm i --save-dev appium chai

wdio.conf.jsファイルの編集

生成された設定ファイルwdio.conf.jsは、そのままではAppiumを使ったテストを書くことができません。 少なくとも以下の修正が必要です。

同時起動数を1にする

WebdriverIOにはテストをできるだけ並行実行するという特徴がありますが、 そのときにAppiumも同じ待ち受けポートで複数個起動してしまい、うまく動作しません。

その事象を回避するため、maxInstances1にしてください。

exports.config = {
  ...
  maxInstances: 1,
  ...
}

Desired Capabilitiesを適切に設定する

設定ファイルの雛形にはFirefoxを利用するDesired Capabilitiesが設定されています。 この部分をAppiumで利用するDesired Capabilitiesに置き換えてください。 たとえば以下のようになります。

exports.config = {
  ...
  capabilities: [{
    platformName: 'Android',
    automationName: 'UiAutomator2',
    app: './app-release.apk',
    deviceName: 'Android Device',
    unicodeKeyboard: true
  }],
  ...
}

capabilitiesにはDesired Capabilitiesの配列を指定するようになっていますが、 配列の要素は1つだけにしてください。 複数の要素を持たせると、書いた要素の数だけAppiumが同時に起動してしまいます。

また、雛形にはcapabilitiesの中にもmaxInstancesが設定されていますが、 こちらも同じ理由で削除しなければなりません。

接続先情報を設定する

Appiumサーバの待ち受けポートのデフォルト値は4723ですが、 WebdriverIOの接続先ポート番号のデフォルト値は異なります。そのため、以下のとおり接続先を明示的に指定してください。

exports.config = {
  ...
  host: 'localhost',
  port: 4723,
  ...
}  

npm testでテストを実行できるようにする

最後に、package.jsonを以下のように変更します。

{
  ...
  "scripts": {
    "test": "wdio",
    ...
  },
  ...
}

これでnpm testコマンドを使ってテストを実行できるようになりました。

テストコードを書くときのポイント

次に、テストコードを書くときに、Appiumだからこそはまるポイントを説明します。 WebdriverIOの詳しい使い方は公式ドキュメントに譲りますが、 次の点を押さえておくと、これから説明する内容が理解しやすくなると思います。

  • テストコードからは、グローバル変数browserにアクセスできます
  • browserが提供するメソッドの一覧はWebdriverIO APIで確認できます
  • browserが提供するメソッドは非同期ですが、前述のwdioテストランナーから呼び出されるときはawaitなしでも同期呼び出しになります
  • UIコンポーネントの操作はbrowser.click(selectorText)のように行います

UIコンポーネントの特定

簡略化された書き方 でUIコンポーネントを探せるのがWebdriverIOの特徴のひとつになっています。 しかし、この書き方はブラウザを操作するときであれば威力を発揮するものの、モバイルアプリではそうはいきません。

特に「テキスト」の判定は、ブラウザを前提とした公式ドキュメントの説明に惑わされてはいけません。 ブラウザで「テキスト」と言うと、<span>この部分</span>を指しますが、 Androidでは<android.widget.Button text="この部分" />になるため、WebdriverIOの簡略記法は使えないのです。

たとえばAndroidの場合は、次のようなユーティリティメソッドを用意しておくと良いでしょう。

module.exports = {
  /** リソースIDを指定するセレクタ */
  resourceId(id) {
    return `//*[@resource-id="${id}"]`;
  },

  /** 表示テキストを指定するセレクタ */
  text(text) {
    return `//*[@text='${text}']`;
  },

  /** contentDescription属性を指定するセレクタ */
  accessibilityId(text) {
    return `~${text}`;
  },

  /** UIコンポーネントのクラス名(android.widget.Buttonなど)を指定するセレクタ */
  className(className) {
    return `android=new UiSelector().className("${className}")`;
  }
};  

このユーティリティメソッドを使えば、 「OKと表示されたUIコンポーネントをクリックする」という操作は次のように書けます。

const by = require(...);
browser.click(by.text('OK'));

スクロール

WebdriverIOには scroll(selectorText)メソッドが存在しており、 ドキュメントには、あるコンポーネントが画面内に入るまでスクロールできると書かれています。

しかし、このメソッドはAppiumでは動作しませんので、自身で実装が必要です。 たとえば、下方向にスクロールする実装は次のようになります。

  scrollDown(selectorText) {
    const screenSize = browser.windowHandleSize().value;
    const width = screenSize.width;
    const height = screenSize.height;
    const isOnScreen = () => browser.isExisting(selectorText) && browser.isVisible(selectorText);
    browser.waitUntil(() => {
      // 既に画面内に有る場合は、スクロールせずに脱出する
      if (isOnScreen()) {
        return true;
      }

      // 画面中央をタップし、少し200pxだけy方向に動かす。
      browser.touchAction([
        {
          action: 'press',
          x: width / 2,
          y: height / 2
        },
        // Appium1.8からはこのwaitが必要
        {
          action: 'wait',
          ms: 500
        },
        {
          action: 'moveTo',
          x: width / 2,
          y: height / 2 + 200
        },
        'release'
      ]);

      return isOnScreen();
    }, 180000);
  }

このサンプルコードでは、最初に端末の画面サイズを取得しています。 その後、目的のUIコンポーネントが画面内に見つかるかタイムアウト(ここでは180秒)するまで、以下の処理を繰り返しています。

  • pressで画面中央をタップします
  • waitで500msec待ちます。この処理はAppium 1.8より必要になりました
  • moveToで、画面中央から200pxだけ下方向に指を動かします
  • releaseで指をリリースします

テスト失敗時に解析しやすくする工夫

テストに失敗したときには、次の情報があると解析が捗ります。

  • Appiumのログ
  • テストに失敗したときのスクリーンショット
  • テストを実行した端末のログ

このうちAppiumのログは、ログレベルをverboseにしておけば標準出力に表示されます。 残りの2つについてAndroidの場合の設定を紹介します。

スクリーンショットを保存する

WebdriverIOはデフォルトで、コマンド(browserが提供している各メソッド)実行に失敗したタイミングでスクリーンショットを保存してくれます2。 ところが、画面に表示されている文字列が想定と異なる場合などではスクリーンショットが保存されません。 そのようなケースではassertには失敗しているものの、コマンドの実行は成功しているからです。

この問題を解決するため、テスト失敗時(assertに失敗したとき)にもスクリーンショットを保存するようにします。 テストが終了する度にwdio.conf.jsafterTest関数(デフォルトではコメントアウトされています)がコールバックされるため、その中にスクリーンショットを保存する処理を書いていきます。

exports.config = {
  ...
  /**
   * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) ends.
   * @param {Object} test test details
   */
  afterTest: async function(test) {
    // エラーでないときは何もせずにreturnする
    if (!test.err) {
      return;
    }

    const fs = require('fs');
    const dateFormat = require('dateformat');
    // ファイル名を現在時刻(秒)で生成するため、重複しないように1秒スリープする
    await browser.pause(1000);
    const dateString = dateFormat(Date.now(), 'isoDateTime');

    // スクリーンショットの保存
    const screenshotFileName = `screenshot-${dateString}.png`;
    await browser.saveScreenshot(`./screenshots/${screenshotFileName}`);
  },
  ...
}  

このサンプルコードでは./screenshots/ディレクトリにscreenshot-2018-10-02T21:58:29+0900.pngのようなファイル名でスクリーンショットを保存しています。 ファイル名に現在時刻を含めるためにdateformatを利用していますので、 あらかじめnpm installコマンドでdateformatをインストールしておいてください。

スクリーンショットを取得しているのはbrowser.saveScreenshot()です。 afterTest関数の中ではbrowserが提供しているAPI呼び出しであっても非同期呼び出しになるため、 明示的にawaitキーワードを付けて処理の終了を待たなければならない点に注意してください。

logcatを保存する

WebdriverIOではbrowser.log()の引数に‘logcat’を指定することで、logcatを取得できます。 スクリーンショットを保存したときと同じく、wdio.conf.jsafterTest関数に処理を追加していきましょう。

前述のスクリーンショット保存コードに続けて、次のコードを追加してください。

    // logcatの保存
    const logcatFileName = `logcat-${dateString}.log`;
    const logcats = await browser.log('logcat');
    logcats.value.map(o => {
      const timestamp = dateFormat(new Date(o.timestamp), 'yyyy-mm-dd HH:MM:ss o');
      const message = o.message;
      return `${timestamp} - ${message}\n`;
    }).forEach(logline => {
      fs.appendFileSync(`./logs/${logcatFileName}`, logline);
    });

browser.logcat()はlogcatの各行(timestampmessageで構成)を要素とした配列を返します。 このサンプルコードでは、各行をyyyy-mm-dd HH:MM:ss +0900 - ログメッセージという形に加工して上で ./logsディレクトリにlogcat-2018-10-02T21:58:29+0900.logのようなファイル名で保存しています。

afterTest関数の全体像

前述の2つの処理を書いたafterTest関数の全体像は次の通りです。 コピーして試してみる場合は、./logsディレクトリと./screenshotsディレクトリを事前に作成しておいてください。

exports.config = {
  ...
  /**
   * Function to be executed after a test (in Mocha/Jasmine) or a step (in Cucumber) ends.
   * @param {Object} test test details
   */
    // エラーでないときは何もせずにreturnする
    if (!test.err) {
      return;
    }
    const fs = require('fs');
    const dateFormat = require('dateformat');
    // ファイル名を現在時刻(秒)で生成するため、重複しないように1秒スリープする
    await browser.pause(1000);
    const dateString = dateFormat(Date.now(), 'isoDateTime');

    // スクリーンショットの保存
    const screenshotFileName = `screenshot-${dateString}.png`;
    await browser.saveScreenshot(`./screenshots/${screenshotFileName}`);

    // logcatの保存
    const logcatFileName = `logcat-${dateString}.log`;
    const logcats = await browser.log('logcat');
    logcats.value.map(o => {
      const timestamp = dateFormat(new Date(o.timestamp), 'yyyy-mm-dd HH:MM:ss o');
      const message = o.message;
      return `${timestamp} - ${message}\n`;
    }).forEach(logline => {
      fs.appendFileSync(`./logs/${logcatFileName}`, logline);
    });
  },
  ...
}  

まとめ

WebdriverIOでAppiumを使ったテストを書くときのノウハウを紹介しました。 WebdriverIOの公式ドキュメントにはAppiumの場合の説明が少ないのですが、 本エントリで紹介した内容を押さえておけば、基本的なテストは書き始められると思います。 参考にしてみてください。


  1. 大変ありがたいことに、業務時間の一部を執筆活動に充てることをリーダーの@okitanさんに快諾していただきました。このような柔軟性がSWETグループの魅力のひとつです!

  2. 保存先はwdio.conf.jsscreenshotPathで指定したディレクトリ(デフォルト値は'./errorShots/')です。

reviewdogによるGoのコードレビュー

こんにちは。ゲーム・エンターテインメント事業本部の鈴木です。
AndAppの開発をしています。

今回は私たちのチームで使っているreviewdogについて、CIの設定やlinterの組み合わせなど、どのようにしてコードレビューに活用しているか紹介します。

reviewdogとは

一言でいうとコードレビューを補助してくれるコマンドラインツールです。
各種linterの実行結果を渡してあげると、Pull Requestなどの差分に関して警告された行を教えてくれます。

私たちのチームではほとんどがGo言語による開発なため、golintなどの結果を表示するために使っています。
開発はGitHub Enterprise上で行なっており、CircleCI Enterpriseからreviewdogを実行すると以下のような警告がPull Requestのレビューコメントとして付きます。

f:id:swet-blog:20180912141758p:plain

似たようなツールであるDangerはPull Request全体に関わるチェックを得意としており、

  • Pull RequestのTitleおよびDescription
  • Pull Requestのファイル数や行数
  • Pull Requestに含まれるファイル名

などを元にする時にはとても便利ですが、linterとの連携という点ではreviewdogの方が簡単に使えます。

なぜreviewdogを導入したのか

以下の3点からreviewdogの導入を決めました。

1. lintの結果をPull Requestのレビューコメントに表示できる

それまでは人が細かい部分までレビューをしていたため、そこに気を取られ過ぎてなかなか本質的なレビューに進めないということがありました。
そうなると双方が、細かい修正やその確認で消耗してしまい、レビュー自体が後回しになりがちという悪循環に陥っていました。
特にレビュアーへの時間や精神的な負担が大きく、その一部でも代行してくれるような仕組みが求められていました。

単純にlintの結果を表示するだけならDangerでも可能ですが、通常のコメントとして表示してしまうと何が警告されたのかわかりにくくなってしまいます。
かといって該当行に警告を表示しようとすると、様々なlinterの出力ごとに該当行を特定したり、APIを呼び出したりといった作り込みをしなければなりません。
reviewdogはそのあたりを吸収してくれるので人の代わりにレビューを行なってくれるツールとしてぴったりでした。

2. 改修のあった部分のみを警告できる

また、対象となるコードは既にサービスとしてリリースされていましたが、それまで一度もlinterをかけたことが無かったので大量の警告がある状態でした。
しかも開発は日々続いており、全てを直してからlinterを導入するのも現実的ではなく、改修のタイミングで段階的に対応していく必要がありました。
当然reviewdogはそのようなユースケースも想定されているのでPull Requestの差分に含まれない行の警告をフィルタリングしてくれます。

3. 任意のlinterを使用できる

最後は任意のlinterをかけたいという要求です。
AndAppではインフラにGoogle Cloud Platformを採用していますが、そのAPIの使い方を誤ってしまうことがありました。
こちらについては人によるチェックだと見逃されてしまうことがあるため、ツールを活用したいということをDeNA TechCon 2018でお話ししました。

しかし、既存のlinterでは私たちがチェックしたい内容を満たすものが無かったため、独自のlinterを作成してチェックするようにしました。
こういったケースでもreviewdogはlinterの出力をどのように扱うのかを簡単かつ柔軟に指定できるため、何の問題もなく独自のlinterを使うことができました。

reviewdogの使い方

Go製のツールなので、

go get -u github.com/haya14busa/reviewdog/cmd/reviewdog

もしくはGitHubのRleasesページからダウンロードしたバイナリをPATHの通った場所に配置することでインストールできます。
(CIで利用する場合、バージョンを固定できるこちらが推奨)

インストール後はREADME.mdやコマンドのhelpに書いてある通り、

$ <linter> | reviewdog [<flags>] (-reporter=github-pr-review | -diff=<diff command>)

の形式で実行できます。

例えばCircleCI上でgolintを実行する場合は以下のようになります。

export GITHUB_API=<GitHub EnterpriseのAPIエンドポイント>
export REVIEWDOG_GITHUB_API_TOKEN=<APIアクセストークン>
golint $(go list ./...) | reviewdog -f=golint -reporter='github-pr-review'

また、Pull Requestをpushする前などにローカルでgolintを実行する場合は以下のようになります。

golint $(go list ./...) | reviewdog -f=golint -diff='git diff master'

ただ、現在は以下にある複数のツールを使用しており、

それぞれに対して実行時にオプションを指定していたりと、個別に実行するのが面倒なのでまとめて実行するスクリプトを用意しています。

  • go-reviewdog.sh
#!/bin/sh

PKGROOT=$1
if [ "$1" = "" ]; then
    PKGROOT="."
fi

REVIEWDOG_ARG="-reporter='github-pr-review'"
if [ "$CI_PULL_REQUEST" = "" ]; then
    REVIEWDOG_ARG="-diff='git diff master'"
fi

golint $(go list $PKGROOT/...) | eval reviewdog -f=golint $REVIEWDOG_ARG

gsc -tests=false \
    -exit-non-zero=false \
    $(go list $PKGROOT/...) \
    | eval reviewdog -f=golint -name="gsc" $REVIEWDOG_ARG

megacheck -tests=false \
    -staticcheck.exit-non-zero=false \
    -simple.exit-non-zero=false \
    -unused.exit-non-zero=false \
    -unused.fields=false \
    $(go list $PKGROOT/...) \
    | eval reviewdog -f=golint -name="megacheck" $REVIEWDOG_ARG

golangci-lint run \
    --issues-exit-code 0 \
    --out-format checkstyle \
    --disable-all \
    -E errcheck \
    -E ineffassign \
    -E interfacer \
    -E unconvert \
    -E misspell \
    -E unparam \
    -E nakedret \
    -E prealloc \
    $GOPATH/src/$PKGROOT/... \
    | eval reviewdog -name=golangci-lint -f=checkstyle $REVIEWDOG_ARG

golangci-lint run --tests=false \
    --issues-exit-code 0 \
    --out-format checkstyle \
    --disable-all \
    -E dupl \
    -E goconst \
    -E gocyclo --gocyclo.min-complexity 15 \
    $GOPATH/src/$PKGROOT/... \
    | eval reviewdog -name=golangci-lint -f=checkstyle $REVIEWDOG_ARG

こちらのスクリプトはローカルで実行する際はmasterとの差分をstdoutに出力します。
他にもBranchのpushなど、Pull Request以外の場合はそのような動作するようになっており、CircleCIでは次のように設定して使っています。

  • .circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: <reviewdogや関連ツール・スクリプトがインストール済みのイメージを指定>

    environment:
      GITHUB_API: <GitHub EnterpriseのAPIエンドポイント>
      # REVIEWDOG_GITHUB_API_TOKEN: CircleCI上のEnvironment Variablesに設定

    steps:
      - checkout

      # 省略

      - run:
          name: Execute reviewdog
          command: go-reviewdog.sh <対象のパッケージ名>

      # 省略

CIで利用するイメージは共通化されており、どのリポジトリでも同じlinterが同じルールで実行されます。
そのため、イメージの方でlinterやルールを変更することで、各リポジトリでは特に何もせずに新しい設定が適用されるようになっています。

linterについて

reviewdogを導入してから半年ほど経ちましたが、実はまだlinterやそのルールがしっかりと固まっていません。
基本的には、

  • Goの標準的なスタイルに合わせる
    • golint
  • シンプルで安定したコードを書く
    • megacheck
  • よくある問題を防ぐ
    • gsc

としていますが、golangci-lintに関しては試行錯誤しながら使っています。
また、一部のlinterは実行時間の短縮や誤検出を減らすためにオプションを変えたりテストを除外しています。
定番のgo vetを入れていませんが、それは修正を必須としているのでreviewdogを通さずに実行しているためです。

試行錯誤した結果、現時点では下表のlinter設定にしています。

Linter Include tests Description
golint - 公式のlinter
gsc ⬜️ スコープ外のContextを使用していないか、などのチェックをする
megacheck staticcheck ⬜️ 誤った動作をするコードがないかチェックする
gosimple ⬜️ より簡潔に書けるコードがないかチェックする
unused ⬜️ 未使用の変数、型、フィールドがないかチェックする
※フィールドのチェックはgoon_kindが引っかかるので無効にしている
golangci-lint errcheck 未チェックのエラーがないかチェックする
ineffassign 値が未使用なまま上書きされている変数がないかチェックする
interfacer より小さいinterfaceに置き換えられる引数がないかチェックする
unconvert 不必要なキャストがないかチェックする
misspell 英単語のスペルに誤りがないかチェックする
unparam 未使用の引数がないかチェックする
nakedret Named Result Parametersによるreturnをしていないかチェックする
prealloc 事前に容量を確保できるスライスがないかチェックする
dupl ⬜️ 重複しているコードがないかチェックする
goconst ⬜️ 定数に置き換えられるコードがないかチェックする
gocyclo ⬜️ コードの循環的複雑度をチェックする
min-complexityを15に設定(デフォルトは30)

この中でよく警告されるのはコメントが無いコードに対してgolintが出力する

exported XXX should have comment or be unexported

ですが、コメントの英語縛りはしていないため、警告されたら日本語でコメントを足せばいいのであまり困っていません。
それにコメントを書くタイミングで書きづらいことに気づいた場合、処理を詰め込みすぎている可能性があるので実装を考え直すきっかけにもなっています。

まとめ

reviewdog導入の成果として「コードの品質を改善しよう」という意識がチーム内で高まりました。

おかげで最低限のチェックが済んだ状態でレビューを始めることができるようになりました。
ただし、一部のlinterは厳しめのチェックをすることもあるため、実装者にとってはやや負担が増えてしまっているようにも感じます。
それでも後々になって致命的な問題が見つかるよりはよっぽど良いと思っています。

また、reviewdogの特徴として改修のあった部分のみが警告されるため、自分の触ったコードやその周辺を綺麗にしようという「ボーイスカウト・ルール」が働きやすくなっています。
これが1度に全ての警告が出てきてしまうと気軽に直すのが難しく、つい後回しにされがちなので狙い通りの効果だと言えます。

今の段階ではまだ誤検出をしてしまったり、警告の意味がわからず戸惑ってしまうようなことがあるような状況です。
しかし、今後もlinterやルールは継続的に見直していくつもりですし、慣れてくると意識しないでも警告されないコードが書けるようになるはずです。
このような積み重ねをしてくことでレビューの効率化、ひいては全体的なGo力の底上げに繋げていきたい考えています。

Google Cloud Buildとは一体何者なのか

f:id:swet-blog:20180814114425p:plain

こんにちは。SWETの加瀬(@Kesin11)です。
Google Cloud Next ’18でGoogleのCI/CDサービスとしてGoogle Cloud Build(以後GCBと略します)というものが発表されました。
https://cloud.google.com/cloud-build/
https://www.youtube.com/watch?v=iyGHW4UQ_Ts

CI/CDサービスを追っているものとして、これは要チェック!
ということで、GCBを軽く使ってみて分かった特徴をCircleCIと比較してまとめてみました。

最初にまとめを書いてしまうと、GCBはCI/CDとして見るとCircleCIと比べてまだまだ扱いづらいところがあります。一方で、従量課金制のフルマネージドなビルドサービスというCircleCIとは違った使い方もできるところが特徴と言えます。

GCBの特徴

1ステップ1コンテナ

一般的なCI/CDサービスでは、以下のようなステップをGUIや専用のファイルに自分で記述していきます。

  • git checkout
  • 依存ライブラリのインストール
  • ビルド
  • テスト実行
  • デプロイ

CircleCIを例にすると、それぞれのステップは1つの仮想マシン、あるいは最近では1つのdockerコンテナ上で実行されるのが一般的です。

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:9
    working_directory: ~/repo

    steps:
      - checkout
      - restore_cache:
          keys:
          - v1-dependencies-{{ checksum "package.json" }}
          - v1-dependencies-
      - run: npm install
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      - run:
          name: lint
          command: npm run lint
      - run:
          name: test
          command: npm run test

上の.circleci/config.ymlの場合、circleci/node:9のコンテナ上で全てのステップが実行されることになります。Workflowsを活用した場合には複数のコンテナを使用してビルドフローを構築することも可能ですが、基本的には1コンテナ上で複数のステップを実行するという考え方です。

GCBではここから根本的に異なっており、1ステップ1コンテナで実行されます。

cloudbuild.yaml

steps:
# build node script
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'lint']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'test']
- name: 'gcr.io/cloud-builders/docker'
- args: ['build', '-t', 'gcr.io/$PROJECT_ID/sampleproject:latest', '.']

各ステップに必ずdockerイメージ名が記載されており、実際にこのコンテナ上で各ステップが実行されます。 そのため、例えばnpmを使いたいときはnpmが使えるコンテナ、docker buildしたいときはdockerが使えるコンテナを指定します。

Cloud Buildではよく使われるコマンドを実行できるdoockerイメージをGitHub上で公式コミュニティがメンテしています。 例えばgcloudkubectlfirebaseなどが使えるイメージが存在します。

もちろん、Cloud Buildによってメンテされているdockerイメージ以外も使用可能です。例えば先ほどのcloud-buildersのnpmのディレクトリにあるREADMEを見ると、以下のようにentrypointを指定することで公式のnodeイメージを使用することも可能であると書かれています。

steps:
- name: node:10.8.0
  entrypoint: npm
  args: ['install']

より詳しく知りたい場合、GCBのCreating Custom Build Stepsのドキュメントを参照するとよいでしょう。
https://cloud.google.com/cloud-build/docs/create-custom-build-steps?hl=en

GitHubのChecks APIが使える

2018/05にGitHubからChecks APIというものが公開されました。 これはPull Requestの画面からCI結果の詳細が見られるようになるという新機能なのですが、まだ対応されているサービスは多くありません。 この記事が公開された2018/08の時点で対応しているサービスはTravis CI、Visual Studio App Center、そしてGCBの3つしかないようです。 https://developer.github.com/v3/checks/

CircleCIは5月の発表時点では名を連ねていましたが、まだ使えるようになっていません。

docker imageのpushが簡単

GCBの特筆すべき特徴の1つとして、Google Cloud Platform(GCP)に含まれるGoogle Container Registry(GCR)との連携が挙げられます。 GCRはプライベートなコンテナレジストリを提供してくれるサービスで、特にGCPのコンテナを扱うサービス(GKEなど)を利用する上ではとても便利です。

GCBでは以下のようにdocker buildしたイメージをimagesで指定するだけで、自動的にGCRにpushしてくれます。 認証のための鍵などを考える必要はなく、非常にお手軽です。

cloudbuild.yaml

steps:
# docker build
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/$PROJECT_ID/sampleproject:latest', '.']

# push built images to Container Registry
images: ['gcr.io/$PROJECT_ID/sampleproject:latest']

ローカルからコマンド実行するだけで手軽にビルドできる

CircleCIなどのCIサービスは基本的にGitHubにpushされたトリガーによってジョブが走るため、最初にビルドフローをトライ&エラーで構築するときに設定ファイルを少し変えてはgit pushをひたすら繰り返すことになります。

GCBの場合はgcloud builds submit --config=cloudbuild.yaml .このコマンド一発だけでローカルのソースコードを全てtarで固めて、cloudbuild.yamlに従ってクラウド上ジョブが実行されるので非常に簡単です。
https://cloud.google.com/cloud-build/docs/running-builds/start-build-manually?hl=en

設定ファイルを修正するたびにgit pushして動作確認するのもそれほど手間ではありませんが、トライ&エラーの時期にはコマンド一発で動作確認ができるのは意外と便利です。また、CI/CDという文脈を外せばリモートでdocker buildなどを実行できる便利なサービスとして使うこともできるでしょう。

ちなみにGCBでも手動で実行するだけではなく主要なCIサービスと同様に、GitHubにpushしたタイミングで自動的にジョブが動くように設定することは可能です。
https://cloud.google.com/cloud-build/docs/run-builds-on-github?hl=en

8コア、32コアマシン

GCBではビルドを実行するマシンのスペックを簡単に選択できます。どれだけ簡単かというと、先程のコマンドに--machine-typeオプションを追加するだけです。

gcloud builds submit --config=cloudbuild.yaml --machine-type=n1-highcpu-8 .

これだけでビルドを実行するマシンは8コアのスペックになります。オプション1つでマシンスペックを上げることが可能なので、非常に時間がかかるビルドフローを実行する場合だけハイスペックマシンにするという使い方が可能です。
もちろんcloudbuild.yamlに直接マシンスペックを記載することで、常に8コアマシンを使用してビルドするように設定することも可能です。
https://cloud.google.com/cloud-build/docs/api/reference/rest/v1/projects.builds#machinetype

従量課金制

CircleCIは月額課金制で、並列数やコンテナ数に応じて料金が変動する料金体系になっています。
https://circleci.com/pricing/

GCBでは料金体系が一般的なCIサービスとは異なっており、月額課金ではなくて従量課金制です。1コアマシンなら毎日120分は無料で、それを超えた先は分単位で課金されます。
ちなみに無料枠は1コアマシンだけのようで、先程紹介した8コア、32コアマシンの場合は無料枠がありませんので注意してください。
https://cloud.google.com/cloud-build/pricing?hl=en

CircleCIとは料金体系が異なるのでどちらが良いとは一概に言えませんが、少なくともGCBの場合はハイスペックマシンが必要な瞬間や、ビルドフローによってマシンスペックを変更することが可能という柔軟性があります。

GCBのイマイチな点

ここまでGCBの特徴として基本的に良い点を取り上げてきましたが、使ってみてイマイチだと感じた点もあるのでこちらについても触れておきます。

Slack連携がない。Cloud Functionでやるしかない

これが痛すぎるマイナスポイント。なんとビルドの成功/失敗を通知するという基本的な機能ですらデフォルトでSlackやメールに対応していません。

ビルドの結果はGoogle Cloud Pub/Subに流されるので、それをトリガーにしたCloud Functionを自前で用意してSlackなりMailgunのようなメール送信サービスのAPIを叩けということのようです。

https://cloud.google.com/cloud-build/docs/send-build-notifications https://cloud.google.com/cloud-build/docs/configure-third-party-notifications

CircleCIのような主要なCI/CDサービスではSlackなどに簡単に接続できるので、この点はだいぶ方針が異なると思いました。

キャッシュの記法が無い

CircleCIでは、ビルド時間を短くするために有効な方法として依存ライブラリのキャッシュがあります。CircleCIではキャッシュの読み取り/保存のためにrestore_cachesave_cacheという特別なステップが用意されています。

GCBの場合には、そのようなキャッシュのためのステップは用意されていないようです。自分がドキュメントを探した限りでは代替手段の解説も見当たりませんでした。
どうしてもキャッシュを使用したい場合は、Google Cloud Storage(GCS)をキャッシュの代わりに使用することで実現できるかも…?

並列ビルドフローの書き方が分かりにくい、フローのGUIが無い

CircleCIでは2.0からWorkflowを使うことで比較的簡単に並列のビルドフローを組み立てることができ、さらにビルドフローの図をGUIで表示してくれます。
https://circleci.com/docs/2.0/workflows/

GCBでも並列のビルドフローを組み立てることは可能ですが、設定ファイルの記述方法がCircleCIと比較してとても把握しやすいとは言えません。

例として、1つのリポジトリの中にツール用のTypeScriptと、Firebase Hostingにデプロイする用のディレクトリが存在する場合に並列で処理させる場合の書き方を比較します。

ディレクトリの構成はこのような感じです。

.
├── firebase.json
├── node_modules
├── package-lock.json
├── package.json
├── ts # ツール用のTypeScript
├── js # build後に生成されるjs
├── tsconfig.json
├── tslint.json
└── web # Firebase HostingにデプロイするNuxt.jsによるウェブサイト
    ├── assets
    ├── components
    ├── dist
    ├── layouts
    ├── middleware
    ├── node_modules
    ├── nuxt.config.js
    ├── package-lock.json
    ├── package.json
    ├── pages
    ├── plugins
    ├── static
    └── store

まずはCircleCI。一連のステップをbuild, build-web, deployのジョブに分割し、最後のworkflowsで順序や依存関係を記述するスタイルです。

以下の設定ではこのような流れになります(キャッシュ周りなど、今回の説明に不要な一部のステップは省略しています)。

f:id:swet-blog:20180814114416p:plain

.circleci/config.yml

version: 2
jobs:
  build:
    docker:
      - image: circleci/node:9
    working_directory: ~/repo

    steps:
      - checkout
      - run: npm install
      - run: npm run lint
      - run: npm run build

  build-web:
    docker:
      - image: circleci/node:9
    working_directory: ~/web

    steps:
      - checkout
      - run:
          name: npm install
          command: npm install
          working_directory: web
      - run:
          name: lint
          command: npm run lint
          working_directory: web

  deploy:
    docker:
      - image: circleci/node:9
    working_directory: ~/repo

    steps:
      - checkout
      - run:
          name: firebase deploy
          command: npm run deploy -- --token "$FIREBASE_TOKEN"

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - build-web
      - deploy:
          requires:
            - build
            - build-web

次がGCBです。ステップごとにidを設定しておき、waitForで依存するステップを記述するスタイルです。
waitForが無いステップでは、自動的に1つの上のステップに依存するという挙動になります。 ただしwaitForで'-'を指定した場合は特別で、どのステップにも依存しないという意味になるので並列化する部分の起点になります。

以下の設定ではbuild webの最初のステップでwaitFor: ['-']を指定することで、build TypeScriptbuild webの一連のステップが並列化され、両方が完了してからfirebase deployが実行されます。

cloudbuild.yaml

secrets:
- kmsKeyName: ****
  secretEnv:
    FIREBASE_TOKEN: ****

steps:
# build TypeScript
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'lint']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'build']
  id: 'script:build'

# build web
- name: 'gcr.io/cloud-builders/npm'
  args: ['install']
  dir: 'web'
  waitFor: ['-']
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'lint']
  id: 'web:lint'

# firebase deploy
- name: 'gcr.io/cloud-builders/npm'
  args: ['run', 'deploy']
  id: 'firebase:deploy'
  waitFor: ['script:build', 'web:lint']
  secretEnv: ['FIREBASE_TOKEN']

個人的には、どの部分が並列に実行されるかがwaitForの挙動を把握していたとしても分かりづらいと感じました。さらに、CircleCIのworkflowと比べてyaml上で全体的なフローが把握しにくいだけではなく、GUIでフロー図を出してくれる機能もないのがマイナスポイントです。

とてもお手軽にマシンスペックを変更可能な点がGCBの長所なのですが、この並列ビルドフローの書きにくさがその長所を活かしにくくしてしまっています。

https://cloud.google.com/cloud-build/docs/configuring-builds/configure-build-step-order?hl=en

設定ファイル中に分岐処理を記述できない

CircleCIのconfig.ymlにはif-elseや、ブランチの指定などによって例えばmasterブランチの場合にだけデプロイのステップを実行する、といった制御を行うことができます。
先ほどのconfig.ymlも最後のworkflowsの部分にfiltersを追加することで、最後のdeployのジョブはmasterブランチの場合のみ実行されるといった制御が可能です。

workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - build-web
      - deploy:
          requires:
            - build
            - build-web
          filters:
            branches:
              only:
                - master

一方で、GCBには1つの設定ファイルの中で条件分岐を行うための記法が存在しません。分岐させたい処理だけシェルスクリプトに記述して実行させるという方法でおそらく実現は可能だと思いますが、全体の見通しは悪くなってしまうでしょう。

代わりにGCBはビルドトリガーを設定する際に、ブランチ名を正規表現で指定してビルドに使用する設定ファイルをcloudbuild.yaml以外にも指定できます。例えば以下のようにブランチ毎に使用する設定ファイルを分けることでブランチ毎のフロー制御が可能です。

  • developブランチ
    • cloudbuild.yaml(lintやテストを行う)
  • masterブランチ
    • cloudbuild_deploy.yaml(デプロイを行う)

f:id:swet-blog:20180815165338p:plain

ただし、CircleCIと異なり設定ファイルを分割する必要があるので、保守性の観点から個人的にはCircleCIの方法の方が好みです。

シークレットキーの扱いが面倒

デプロイを実行するフローを構築する場合に、秘密にしておく必要があるトークンをどうやってCIサービス中に読み取るかという問題があります。トークンが記されたファイルをリポジトリに含めてしまうのが最も簡単ですが、セキュリティ的な観点からは望ましくありません。
CircleCIではconfig.yamlとは別にCircleCIのGUIから環境変数の登録が可能なため、秘密にしておきたいトークンなどは環境変数にセットしてしまうのが一般的な方法です。

GCBの場合でも環境変数を使うというアプローチは可能なのですが、GCB単体にはその機能は存在せず、Cloud Key Management Service(Cloud KMS)というまた別のサービスと連携させる必要があります。
先程のcloudbuild.yamlでは、secrets:の箇所で暗号化されたトークンを環境変数としてセットしています。
https://cloud.google.com/cloud-build/docs/securing-builds/use-encrypted-secrets-credentials?hl=en#encrypting_an_environment_variable_using_the_cryptokey

詳しい使い方は上記のドキュメントを参照してください。正直、環境変数を登録したいだけなのにCircleCIと比較すると手順が多いという印象です。

まとめ

他のCI/CDサービス(今回は代表としてCircleCI)と比較したときのGoogle Cloud Buildの良い点、イマイチな点を紹介しました。最後にまとめとして、自分の主観ですがざっくりとした比較表を用意してみました。

CircleCI Google Cloud Build
ビルドフローの書きやすさ
GitHub連携
dockerコンテナのデプロイ(GCPとの連携)
チャットツール連携
マシンパワー
料金 月額課金 従量課金

GCBをCircleCIと比較してみて、個人的にはCI/CDとしてはまだ発展途上かなという印象でした。GCPとの連携は強みですが、ビルドフローが書きにくかったり、Slack通知がデフォルトでは無いあたりが現時点では少々扱いづらい点です。

一方で、コマンド一発でソースコードをアップロードしてクラウドでビルドができたり、マシンスペックを簡単に変更できるところが特徴的で、Cloud Buildというその名の通りフルマネージドな従量課金ビルドサービスとして非常に便利だと思います。

ブランチ戦略によってビルドやテスト、デプロイを複雑に制御するCI/CD用途よりもdocker buildを並列に行ってコンテナレジストリに登録したり、ソースコードのコンパイルといった、シンプルにコア数の多いハイスペックマシンを用意できると時間が短縮できるタスクを行う方が向いているという印象です。

近い将来、もはやビルド用のハイスペックマシン実機を手元に置いておく必要はなくなるかもしれません。
コーディング環境はコード補完が不自由ない程度のスペックのPCを使い、ビルドしたいときだけGCBにソースコードを送りつけてビルドしてもらう。そんな未来が来るかもしれないですね。