Slide 1

Slide 1 text

CIでAndroidUIテストの様⼦を 録画してみた .PCJMFษڧձ8BOUFEMZºνʔϜϥϘ NLFFEB

Slide 2

Slide 2 text

About me • mkeeda (向井⽥ ⼀平) • Twitter: @mr_mkeeda • Github: @mkeeda • Android Engineer at Cybozu, Inc 2

Slide 3

Slide 3 text

UIテストあるある • 不安定🤮 • CIでだけUIテストが落ちる🤮 • エラーがよくわからない🤮 • エラーが毎回違う🤮 3

Slide 4

Slide 4 text

UIテストの様⼦を録画してみた • CIのテスト実⾏時は 
 Androidエミュレータの画⾯が⾒れない • UIテストが落ちるときの画⾯の状態を知りたい • Firebase test labは使ってない • テスト失敗時にGithub Actionsの 
 アーティファクトに録画データを残す 4

Slide 5

Slide 5 text

Androidデバイス画⾯収録の基本 • 録画開始 • adb shell screenrecord <ファイルパス> • ※ファイルパスはAndroidデバイスのパス • 録画停⽌ • Ctrl + C (mac は Command + C) • 録画ファイル取得 • adb pull <録画ファイルのパス> <ローカルデバイスの保存先パス> 5

Slide 6

Slide 6 text

テストケースごとに録画してみる • ScreenRecordRule を作る • “record_<テストメソッド名>.mp4” というファイル名で保存する 6 @RunWith(AndroidJUnit4::class) class SampleUiTest { @get:Rule var activityRule: ActivityScenarioRule = ActivityScenarioRule(LoginActivity::class.java) @get:Rule var screenRecordRule = ScreenRecordRule() @Test fun test() { // run test } }

Slide 7

Slide 7 text

テストコードから adb コマンドを実⾏ • UiAutomation.executeShellCommand(String command) を使う • ローカルマシンで adb shell をするのと同等 7 val uiAutomation = InstrumentationRegistry.getInstrumentation().uiAutomation // ࿥ը։࢝ uiAutomation.executeShellCommand("screenrecord /sdcard/record.mp4") // ࿥ըऴྃ (Ctrl + C) ϓϩηεID͕ඞཁ uiAutomation.executeShellCommand("kill -SIGINT $screenRecordProcessId")

Slide 8

Slide 8 text

screenrecord のプロセスを得る 8 fun executeCommand(cmd: String): String { val parcelFileDescriptor = uiAutomation.executeShellCommand(cmd) return ParcelFileDescriptor.AutoCloseInputStream(parcelFileDescriptor).use { inputStream -> inputStream.readBytes().toString(Charset.defaultCharset()) } } fun findProcessIds(processName: String): List { return executeCommand(cmd = "pidof $processName") .trim() .split(Regex("\\s+")) .filter { it.isNotEmpty() } .map { it.toInt() } } val screenRecordProcessIds = findProcessIds(processName = "screenrecord")

Slide 9

Slide 9 text

ScreenRecordRule 9 class ScreenRecordRule : TestWatcher() { private val shell = Shell() private var screenRecordProcessIds: List = emptyList() override fun starting(description: Description) { shell.executeCommand(cmd = "screenrecord /sdcard/record_${description.methodName}.mp4", awaitOutput = false) screenRecordProcessIds = shell.findProcessIds(processName = "screenrecord") } override fun finished(description: Description) { // গ͠஗Ԇ͔ͤͯ͞Βऴྃͤ͞ͳ͍ͱ࠷ޙ·Ͱ࿥ըͰ͖ͳ͍ͱ͖͕͋ͬͨͷͰ࢓ํͳ͘ Thread.sleep(5000) // ͢΂ͯͷscreenrecordϓϩηεΛࢭΊΔ // 1σόΠεͰ࣮ߦ͍ͯ͠Ε͹ଞͷςετέʔεͷ࿥ըΛࢭΊͯ͠·͏Մೳੑ͸௿͍͸ͣ screenRecordProcessIds.forEach { pid -> shell.executeCommand(cmd = "kill -SIGINT $pid", awaitOutput = false) } } }

Slide 10

Slide 10 text

Github Actionsでの録画データの回収 • テストが失敗したときだけ録画をCIのアーティファクトとして保存する 10 - name: Run android tests uses: reactivecircus/android-emulator-runner@v2 with: api-level: 31 arch: x86_64 disable-animations: true script: | ./gradlew connectedDebugAndroidTest || (mkdir screen_record; adb shell 'ls sdcard/record_*.mp4' | tr -d '\r' | xargs -I% adb pull % ./screen_record && exit 1) - name: Save screen record if: failure() uses: actions/upload-artifact@v2 with: name: screen-records path: ./screen_record

Slide 11

Slide 11 text

• adbコマンドを使えばAndroidTestの実⾏の様⼦を 
 録画できる • 実⾏時の様⼦を知って 
 テスト失敗時の原因追求ができる • 乱⽤するとテスト時間の増⼤に繋がりそう • ScreenRecordRuleのソースコード 
 https://gist.github.com/mkeeda/30b8cfdcec53859a2cd39cac36d7fc9e 11 まとめ To Be Continued

Slide 12

Slide 12 text

参考 • Android Debug Bridge (adb) | Android Developers 
 https://developer.android.com/studio/command-line/adb • UiAutomation | Android Developers 
 https://developer.android.com/reference/android/app/ UiAutomation#executeShellCommand(java.lang.String) • androidx.benchmark.Shell 
 https://cs.android.com/androidx/platform/frameworks/support/+/ androidx-main:benchmark/benchmark-common/src/main/java/androidx/ benchmark/Shell.kt 12