inline Choose(n) {
// 他のプログラミング言語でよく見るif文とは意味が異なり、
// "::"で始まる3つの文のうち、非決定的に(ランダムに)実行される
// 網羅的な検査をするときは、ここで取りうるすべてのパターンが探索される
if
:: n = 1
:: n = 2
:: n = 3
fi
}
active proctype P() {
int a = 0, b = 0
Choose(a) // 上で定義しているChooseがインライン展開され、aには1から3のいずれかの値が代入される。
Choose(b)
assert(a * b <= 9)
}
モデル検査の仕組みがわかったところで、プロダクトに適用できるのかどうかを確かめるために、いくつかの簡単なPoCを作ってみることにしました。
PoCとは、Proof of Conceptの略で、ここではモデル検査の実用性を確かめるために、簡単なデモを作ることを指しています。
PoCの題材決めに際しては、プロダクトに適用する可能性も加味し、プロダクト開発でよく起きる設計・実装上の問題にフォーカスすることを心がけました。
出力された結果を見ると、53行目のinsertの中で処理が先に進めなくなったことがわかります。
2つのプロセスP1、P2がgap0に対しselect for updateで共有ロックをとったものの、お互いinsertに進行ができなくなったようです。
これがMySQLでいうところのdeadlockに相当します。
android {
defaultConfig {
// Use AndroidX test runner
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"// 2) Connect JUnit 5 to the runner
testInstrumentationRunnerArgument "runnerBuilder", "de.mannodermaus.junit5.AndroidJUnit5Builder"
}
// 3) Java 8 is required, add this even if minSdkVersion is 26 or above
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
// 4) JUnit 5 will bundle in files with identical paths; exclude them
packagingOptions {
exclude("META-INF/LICENSE*")
}
}
// 5) (Optional) If use ParameterizedTest ArgumentsProvider, this is required for set to recompile with "-jvm-target 1.8"
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
// 6) Jupiter API
androidTestImplementation "org.junit.jupiter:junit-jupiter-api:5.4.2"// 7) (Optional) Jupiter Parameters
androidTestImplementation "org.junit.jupiter:junit-jupiter-params:5.4.2"// 8) JUnit 5 instrumentation companion libraries
androidTestImplementation "de.mannodermaus.junit5:android-test-core:1.0.0"
androidTestRuntimeOnly "de.mannodermaus.junit5:android-test-runner:1.0.0"
androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
}
@Testfun testExampleUseScenario() {
// use scenarioExtension to get ActivityScenarioval scenario = scenarioExtension.scenario
// then get Activity just same as JUnit 4
scenario.onActivity { activity ->
assertEquals(0, activity.getClickCount())
}
}
3. Use ActivityScenario with "JUnit 5 Parameter Resolution" feature
// A scenario can be passed into a test method directly, when using the ActivityScenarioExtension@Testfun testExampleWithParameter(scenario: ActivityScenario<ActivityOne>) {
scenario.onActivity {
assertEquals(0, it.getClickCount())
}
}
// repeat this test 3 times// combines a custom display name pattern of each repetition via the name attribute@RepeatedTest(value = 3, name = "{currentRepetition}/{totalRepetitions} clickCount={currentRepetition}, expected=''{currentRepetition}''")
fun repeatedTestExample(repetitionInfo: RepetitionInfo) {
val count = repetitionInfo.currentRepetition
for (i in0 until count) {
onView(withId(R.id.button)).perform(click())
}
onView(withId(R.id.textView)).check(matches(withText(count.toString())))
}
// define enum for show action description on test resultsenumclass Action(val rawValue :ViewAction) {
Click(click()),
DoubleClick(doubleClick()),
LongClick(longClick())
}
// define custom arguments class as an implementation of ArgumentsProvider// ArgumentsProvider must be declared as either a top-level class or as a static nested classprivateclass ButtonTestArguments : ArgumentsProvider {
overridefun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of(
Arguments.arguments(Action.Click, 1),
Arguments.arguments(Action.DoubleClick, 2),
Arguments.arguments(Action.LongClick, 1)
)
}
@ParameterizedTest// @ArgumentsSource is one of the Sources of Arguments Annotation@ArgumentsSource(ButtonTestArguments::class)
// get parameters(action,expected) from the Sources of Arguments named ButtonTestArguments// can use both of parameterized arguments(action, expected) and extensions parameter resolution(scenario) at the same timefun parameterizedTestExample(action: Action , expected: Int, scenario: ActivityScenario<ActivityOne>) {
onView(withId(R.id.button)).perform(action.rawValue)
scenario.onActivity {
assertEquals(expected, it.getClickCount())
}
}
発表内容のメインは、それなりの人数で開発されているであろうアプリのリリース作業について人間が調整するのにコストが高くなりすぎたので、自動的に毎週リリースされるフローを構築したというものでした。
その中でも個人的に最も興味を持ったのはGoogle Play Consoleのリリース前レポートでした。
発表では、このリリース前レポートを単純なクラッシュ検知として使うことは残念ながら諦めたという話でしたが、発表後に直接お話を伺ったところFirebase Test LabのRoboテストを直接利用することで同等のクラッシュ検知と通知は実現しているとのことでした。ドキュメントによるとPlay Consoleで使用されているのはこのRoboテストのようです。
SWETグループでは、DroidKaigiでの登壇に限らず、積極的に対外的なアウトプットを行っています。
加えて、SWETグループ主催の勉強会である、Test Night(テストについて語り合う勉強会)も定期的に開催しております。
直近では、CI/CD Test Night、Android Test NightやiOS Test Night(4月開催予定)の開催を予定しております。
ご興味のある方は是非ご参加ください。
Firebase Test Labを使うにはアカウントの認証と、紐付けるプロジェクトの選択が必要になります。
まずはドキュメントに従ってgcloud auth login、gcloud config set project PROJECT_IDを実行してください。PROJECT_IDはfirebaseのプロジェクトIDです。もしまだfirebaseのプロジェクトを作成していない場合、先にfirebaseのプロジェクトを作成しておいてください2。
$ gcloud firebase test ios run --test MyTests.zip \--devicemodel=iphonex,version=12.0,locale=ja_JP \--devicemodel=iphone8,version=11.4 \--devicemodel=iphone6splus,version=9.1 \--devicemodel=iphone6s,version=10.3,locale=ja_JP,orientation=landscape \--devicemodel=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のページで結果を見ることができます。
実行が完了するとこのように複数のデバイスでテストケースごとのテスト結果、スクリーンショットや動画を確認できます。
gcloudコマンドから使う(CI環境)
ローカルの環境から実行できることは確認できましたが、アプリのビルドやテストはローカルとは別のCI環境で実行していると思いますので、継続的にテストを実行させるにはCI環境からFirebase Test Labを実行をできるようにしておくのがよいでしょう。
しかし、先述のgcloud firebase test iosを実行するにはアカウントの認証が必要です。そして認証にはブラウザでログインする必要があるので通常の方法ではCI環境で認証することはできず、テストを実行できません。
#!/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}"\--devicemodel=iphone8,version=11.4 \--devicemodel=iphonex,version=12.0,locale=ja_JP \--devicemodel=iphone6splus,version=9.1 \--devicemodel=ipad5,version=12.0 \--devicemodel=iphone6s,version=10.3,locale=ja_JP,orientation=landscape
# fastlane-plugin-firebase_test_labのREADMEから転載
scan(
scheme: 'YourApp', # XCTest schemeclean: true, # Recommended: This would ensure the build would not include unnecessary filesskip_detect_devices: true, # Requiredbuild_for_testing: true, # Requiredsdk: 'iphoneos', # Requiredshould_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 namedevices: [ # Device(s) to run tests on
{
ios_model_id: 'iphonex', # Device model ID, see gcloud command aboveios_version_id: '11.2', # iOS version ID, see gcloud command abovelocale: 'en_US', # Optional: default to en_US if not setorientation: 'portrait'# Optional: default to portrait if not set
}
]
)
Bitrise、gcloudコマンド、FastlaneのそれぞれでFirebase Test Labによる実機テストを行う方法の解説をしました。Firebase Test Labはとても簡単に使えて、無料プランもあるのでこの手のサービスの中ではかなり試しやすいサービスだと思います。
ビルドのたびに様々なiOSデバイス、iOSバージョンの実機で自動テストを実行することが理想ですが、人手を介さずにそれを全自動で行ってくれるシステムを構築・運用することはとても難しいです。
そのようなシステム構築やデバイスの管理をする必要がないFirebase Test Labを使って、ゼロ円から実機の自動テストを始めてみませんか?
ここで有効化しなかった場合でもサービスアカウントで認証後に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としても大丈夫なようです。↩