ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション開発チームのテックブログです

Appiumで期待した画面が表示されていることを検証する

ども。LDI品質管理部の仙波です。
スマホアプリの自動テストを担当しています。

最近はリモートワークの影響でサイズアップしたのでダイエットを画策中ですが、なかなかうまくいきません。

さて、Appiumの使い方について少し実践的に学んでいきましょう。
今日は期待した画面が表示されているかどうかを判定する方法を紹介します。

説明に使用する画面

Androidの「設定アプリのトップ画面」と、「接続済みのデバイス画面」を使って説明します。

f:id:wave1008:20210427105207p:plain

画面を判定する方法のパターン

一般的に画面のテストは以下のケースの繰り返しです。

  • [事前条件] テスト対象の画面が表示されていることを検証する
  • [アクション] テスト対象のアクション(テキスト入力、ボタン押下など)を実行する
  • [事後条件] 画面や項目が期待値と一致することを検証する


Appiumでは画面要素を取得し、その属性(text属性など)の値を調べるためのAPIが用意されていますが、画面が表示されているかどうかを判定するための直接的なAPIはありません。

したがって、テストコードの作成者は用意されたAPIを使って画面の状態を調べ、期待した画面が表示されているかどうかを判定する必要があります。

画面を判定するにはいくつかの方法がありますが、代表的なものを以下に紹介します。

開発者によって付与された画面に固有のIDを利用する方法

Androidであればresource-id, iOSであればname属性などに、開発者がその画面に固有のIDを付与している場合は、その要素を取得できるかどうかで判定するのが最も簡単です。


@Test
fun getByUniqueId() {

    // Androidの「設定アプリのトップ画面」が表示されます。
    val d = getAppiumDriver()

    val uniqueId = "com.android.settings:id/search_bar"
    // 開発者によって画面に固有の情報がresource-id等に付与されている場合は、その情報を利用して画面を判定できます。
    // ※注:上記のidは実際には画面に固有ではありません。

    val e = d.findElementById(uniqueId)    // 期待する画面が表示されていれば取得に成功します。
    d.quit()
}


実際には、全ての画面にそのような便利なIDが付与されているとは限らないので、他の方法を使用する必要があります。

※サンプルのソースコードの全体はこの記事の最後に記載してあります。


画面タイトルを利用する方法

画面に固有のタイトルが付与されている場合は、これを利用することができます。

@Test
fun getByText() {

    // Androidの「設定アプリのトップ画面」が表示されます。
    val d = getAppiumDriver()

    val text = "接続済みのデバイス"
    val e = d.findElementByXPath("//*[@text='$text']")
    // 画面上に「接続済みのデバイス」が表示されていれば取得に成功します。
    // その画面が「設定アプリのトップ画面」なのか、「接続済みのデバイス画面」なのかは、この方法では区別できません。
    d.quit()
}


上記のように、単純にタイトルのテキストが取得できるかどうかを調べるだけでは、
メニューのテキストなのか、画面タイトルのテキストなのかを区別することができません。つまり、「設定アプリのトップ画面」と「接続済みのデバイス画面」を区別することができません。


厳密に画面タイトルのテキストを取得するには画面の構造に着目します。

f:id:wave1008:20210427103926p:plain


アクションバーの内部にタイトルのテキストが存在していることがわかります。
この構造を利用し、以下のように要素を取得するとよいです。

@Test
fun getByTitle() {

    // Androidの「設定アプリのトップ画面」が表示されます。
    val d = getAppiumDriver()

    // 「設定アプリのトップ画面」で「接続済みのデバイス」のメニューをタップします。
    val e = d.findElementByXPath("//*[@text='接続済みのデバイス']")
    e.click()
    Thread.sleep(1000)

    // 「接続済みのデバイス画面」で画面のタイトルに「接続済みのデバイス」が表示されているかどうかを検証します。
    val title = "接続済みのデバイス"
    val e2 = d.findElementByXPath(
        "//*[@resource-id='com.android.settings:id/action_bar']/descendant::*[@text='$title']"
    )
    // action_barの内部に「接続済みのデバイス」のテキストを持つ要素が存在する場合は取得に成功します。
    d.quit()
}

画面を特徴づける要素が全て取得できることを確認する方法

上記の2つの方法は要素取得が1回で済むので、画面を判定する方法として比較的スマートなやり方です。

どちらの方法も利用できない場合は、要素取得を複数回実行し、その画面を特徴付ける(ユニークに識別できる)要素が全て表示されていることをもって、画面が表示されていると判定することができます。

たとえば「接続済みのデバイス画面」には「接続済みのデバイス」「接続の設定」という項目が両方存在するので、両方の取得に成功したら「接続済みのデバイス画面」が表示されていると判定することができます。

@Test
fun getByUniqueElements() {

    // Androidの「設定アプリのトップ画面」が表示されます。
    val d = getAppiumDriver()

    // 「設定アプリのトップ画面」で「接続済みのデバイス」のメニューをタップします。
    val e = d.findElementByXPath("//*[@text='接続済みのデバイス']")
    e.click()
    Thread.sleep(1000)

    // 期待する画面が表示されていれば取得に成功します。
    d.findElementByXPath("//*[@text='接続済みのデバイス']")
    d.findElementByXPath("//*[@text='接続の設定']")

    d.quit()
}

サンプルのソース

今回使用したサンプルのソースの全体は以下です。


build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.4.32"
}

group = "me.user"
version = "1.0-SNAPSHOT"

val appiumClientVersion = "7.1.0"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")

    // Appium
    testImplementation("io.appium:java-client:$appiumClientVersion")

    // Assert J
    testImplementation("org.assertj:assertj-core:3.11.1")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "13"
}


ScreenTest1.kt

import io.appium.java_client.MobileElement
import io.appium.java_client.android.AndroidDriver
import org.junit.jupiter.api.Test
import org.openqa.selenium.remote.DesiredCapabilities
import java.net.URL

class ScreenTest1 {

    private fun getAppiumDriver(): AndroidDriver<MobileElement> {

        // DesiredCapabilities
        val capabilities = DesiredCapabilities()
        capabilities.setCapability("automationName", "UiAutomator2")
        capabilities.setCapability("platformName", "Android")
        capabilities.setCapability("platformVersion", "10")
        capabilities.setCapability("appPackage", "com.android.settings")
        capabilities.setCapability("appActivity", "com.android.settings.Settings")

        // appiumServerAddress
        val appiumServerAddress = URL("http://127.0.0.1:4723/wd/hub")

        // appiumDriver
        return AndroidDriver<MobileElement>(appiumServerAddress, capabilities)
    }

    @Test
    fun getByUniqueId() {

        // Androidの「設定アプリのトップ画面」が表示されます。
        val d = getAppiumDriver()

        val uniqueId = "com.android.settings:id/search_bar"
        // 開発者によって画面に固有の情報がresource-id等に付与されている場合は、その情報を利用して画面を判定できます。
        // ※注:上記のidは実際には画面に固有ではありません。

        val e = d.findElementById(uniqueId)    // 期待する画面が表示されていれば取得に成功します。
        d.quit()
    }

    @Test
    fun getByText() {

        // Androidの「設定アプリのトップ画面」が表示されます。
        val d = getAppiumDriver()

        val text = "接続済みのデバイス"
        val e = d.findElementByXPath("//*[@text='$text']")
        // 画面上に「接続済みのデバイス」が表示されていれば取得に成功します。
        // その画面が「設定アプリのトップ画面」なのか、「接続済みのデバイス画面」なのかは、この方法では区別できません。
        d.quit()
    }

    @Test
    fun getByTitle() {

        // Androidの「設定アプリのトップ画面」が表示されます。
        val d = getAppiumDriver()

        // 「設定アプリのトップ画面」で「接続済みのデバイス」のメニューをタップします。
        val e = d.findElementByXPath("//*[@text='接続済みのデバイス']")
        e.click()
        Thread.sleep(1000)

        // 「接続済みのデバイス画面」で画面のタイトルに「接続済みのデバイス」が表示されているかどうかを検証します。
        val title = "接続済みのデバイス"
        val e2 = d.findElementByXPath(
            "//*[@resource-id='com.android.settings:id/action_bar']/descendant::*[@text='$title']"
        )
        // action_barの内部に「接続済みのデバイス」のテキストを持つ要素が存在する場合は取得に成功します。
        d.quit()
    }

    @Test
    fun getByUniqueElements() {

        // Androidの「設定アプリのトップ画面」が表示されます。
        val d = getAppiumDriver()

        // 「設定アプリのトップ画面」で「接続済みのデバイス」のメニューをタップします。
        val e = d.findElementByXPath("//*[@text='接続済みのデバイス']")
        e.click()
        Thread.sleep(1000)

        // 期待する画面が表示されていれば取得に成功します。
        d.findElementByXPath("//*[@text='接続済みのデバイス']")
        d.findElementByXPath("//*[@text='接続の設定']")

        d.quit()
    }
}