【Android】AppiumでAndroidのテストを自動化する

#回帰テスト

Appiumを使ってAndroidアプリ回帰テスト用アプリを作成しました

インストール方法に関しては解説してくれているサイトが山ほどあるので割愛します

appium -v 1.8.1

Rubyクライアントです

appiumの公式ドキュメント
http://appium.io/docs/en/writing-running-appium/caps/

Rubyクライアントであるappium_libの公式ドキュメント
https://www.rubydoc.info/github/appium/ruby_lib/index

##アプリをエミュレータ（or実機）にインストール
```hogepium.rb
require "rubygems"
require "appium_lib"
require "date"
include Appium

class Hogepium
  # summary イニシャライザ
  def initialize
    @wait_time = 30
    @package = "androidmanifest.xmlのpackageと同じもの(例：jp.co.fuga.hogeapp)"
    @desired_caps = {
      caps: {
        appPackage: @package,
        appActivity: "androidmanifest.xmlに設定してある一番最初に起動するactivityをパッケージつきで設定する(例：jp.co.fuga.hogeapp.activity.FirstActivity)",
        platformName: "Android",
        app: "ビルドしたapkファイルをフルパスで設定する",
        automationName: "Appium",
        deviceName: "Android Emulator",
        # 日本語入力に必要な設定
        unicodeKeyboard:  'true',
        # avdマネージャで自分が作成したavd名のうちスペースをアンダーバーで置き換え設定する
        avd: "Pixel_XL_API_27"
      },
      appium_lib: {
        # この設定値はよくわからない
        wait: 0
      }
    }
  end

  # ドライバをスタートする（アプリをエミュレータにインストールする）
  def start_driver
    # この2つめのパラメータもよくわからない
    @driver = Driver.new(@desired_caps, false).start_driver
  end
end
```

```hogescenario.rb
require_relative "./hogepium.rb"

h = Hogepium.new
h.start_driver
```

```$ruby hogescenario.rb```で指定したエミュにアプリがインストールアンド起動されます
~~**先にAndroid Studioで対象のエミュを起動させておくこと。**Appiumはエミュの起動までやってくれないので~~
**別記事で見かけました。設定項目のうち
deviceName: "avd managerのname", 
platformVersion: "対象エミュレータにインストールしてあるOSのバージョン（例：7.1.1）"
ではなく
deviceName: "Android Emulator", 
avd: "avdマネージャで自分が作成したavd名のうちスペースをアンダーバーで置き換えたもの"を設定することでエミュレータもappiumが起動してくれました**

また、パッケージをインスタンス変数にした理由は後述します

色々なセレクタを追加していく前に便利メソッドをhogepiumに追加しておきます

```hogepium.rb

  # 現在のページをDOM形式で取得できるプロパティ。シナリオ途中で失敗したときとかにコンソールに出力させると、指定したidやxpathなんかが間違っていないか確認できる
  def page_source
    @driver.page_source
  end

  # 現在のactivityが取得できるプロパティ。うまくつかえば画面遷移したかどうかをハンドリングできるかと思ったけんどFragment使っていたらダメですね
  def current_activity
    @driver.current_activity
  end

  # エミュレータで（実機でも可）表示している画面のスクショ撮って保存できる
  def screen_shot
    @driver.save_screenshot("/任意のパス/#{Time.now}.png")
  end
```

##色々なセレクタを実装していく
```hogepium.rb
  private

  # Viewに設定されているIDを基に要素を取得
  def selector_from_id(id)
    @driver.find_elements(:id, id).first
  end

  # Viewに設定されているIDを基に要素が見つかるまでスクロールして取得する
  # Androidはメモリを節約するために画面範囲外の部分は何も描画していない。
  # そのためfind_elements(:id）だけでは目的の要素を取得することができないためこのセレクタは超大事
  # このセレクタを何度か使い画面中程から上にある要素を探したい場合でも、「一番下までスクロール -> 見つからなかったら上方向にスクロール」を自動でやってくれる。
  # 先ほどのパッケージはここで使う
  def selector_from_uiselector_from_id(id)
    @driver.find_elements(:uiautomator, "new UiScrollable(new UiSelector().scrollable(true).instance(0)).scrollIntoView(new UiSelector().resourceId(\"#{@package}:id/#{id}\"));").first
  end

  # TextViewに設定されているテキストを基に要素を取得する
  # ListViewの目的行を取得したりする際に使う
  def selector_from_textview_in_text(text)
    @driver.find_elements(:xpath, "//android.widget.TextView[@text=\"#{text}\"]").first
  end

  # Buttonに設定されているテキストを基に要素を取得する
  # IDが設定されていないButtonを特定する際に使う
  def selector_from_Button_in_text(text)
    @driver.find_elements(:xpath, "//android.widget.Button[@text=\"#{text}\"]").first
  end

  # 画面に設置されているEditTextを上から数えて指定したposition番目のものを取得する
  def selector_from_view_edittext(position)
    @driver.find_elements(:xpath, "//android.widget.EditText[#{position}]").first
  end

  # selector_from_view_edittextのTextViewバージョン
  def selector_from_view_textview(position)
    @driver.find_elements(:xpath, "//android.widget.TextView[#{position}]").first
  end

  # 同上
  def selector_from_view_button(position)
    @driver.find_elements(:xpath, "//android.widget.Button[#{position}]").first
  end
```

##色々なイベントを実装していく
```hogepium.rb
  private

  # to_rightと書いていますがstart_nとoffset_nの値変えればどの方向でもスワイプできます
  def swipe_to_right(element)
    start_x = element.rect.x + element.rect.width / 10
    start_y = element.rect.y + element.rect.height / 2
    offset_x = element.rect.width - element.rect.width / 10 - start_x
    offset_y = 0
    TouchAction.new(@driver).swipe(start_x: start_x, start_y: start_y, offset_x: offset_x, offset_y: offset_y, duration: 1000).perform
  end

  # EditTextにstrを入力する
  def input(element, str)
    element.send_keys(str)
  end

  def click(element)
    element.click
  end
```

##エンジンを実装する
```hogepium.rb

  private

  def get_element(wait_time, &get_element)
    start_time = Time.now
    while Time.now - start_time < wait_time
      begin
        element = yield
        if !element.nil? && element.enabled? && element.displayed?
          break
        end
      rescue => e
        # p e.to_s
        # p e.message.to_s
      end
    end

    if element
      element
    else
      p "対象Element見つからず!!!"
      # p page_source
      @driver.quit
      exit
    end

  end
```

##ロガー
```logger.rb

class Logger

  def self.debug(classobj, method, *args)
    o = "【" << classobj.class.name << "#"<< method.to_s << "】:"
    if args
      o << args.join(", ")
    end
    p o
  end

end

```


##シナリオ作成に必要なセレクタとイベントを組み合わせたものを色々と実装する
```hogepium.rb
  # summary IDを基にElementを検索しクリックする
  # param id Viewに設定されているID
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def click_from_id(id, wait_time = @wait_time)
    Logger::debug(self, __method__, id)
    click(get_element(wait_time) do
      selector_from_id(id)
    end)
    screen_shot
  end

  # summary 初期表示時点では画面に表示されていないViewをスクロールして探しクリックする
  # param id Viewに設定されているID
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def click_from_id_scroll(id, wait_time = @wait_time)
    Logger::debug(self, __method__, id)
    click(get_element(wait_time) do
      selector_from_uiselector_from_id(id)
    end)
    screen_shot
  end

  # summary TextViewに設定されている文字列を基に検索しクリックする
  # param text 検索対象文字列
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def click_from_textview_in_text(text, wait_time = @wait_time)
    Logger::debug(self, __method__, text)
    click(get_element(wait_time) do
      selector_from_textview_in_text(text)
    end)
    screen_shot
  end

  # summary Buttonに設定されている文字列を基に検索しクリックする
  # param text 検索対象文字列
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def click_from_button_in_text(text, wait_time = @wait_time)
    Logger::debug(self, __method__, text)
    click(get_element(wait_time) do
      selector_from_Button_in_text(text)
    end)
    screen_shot
  end

  # summary 上から数えてposition番目のEditTextに対して文字を入力する
  # param position 対象画面内の上から数えて何番目のEditTextViewか 1から開始
  # param num 入力文字列
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def input_from_view_to_edittext(position, str, wait_time = @wait_time)
    Logger::debug(self, __method__, position, str)
    input(get_element(wait_time) do
      selector_from_view_edittext(position)
    end, str)
    screen_shot
  end

  # summary IDを基に検索したEditTextに対して文字を入力する
  # param position 対象画面内の上から数えて何番目のEditTextViewか 1から開始
  # param num 入力文字列
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def input_from_id_to_edittext(id, str, wait_time = @wait_time)
    Logger::debug(self, __method__, id, str)
    input(get_element(wait_time) do
      selector_from_id(id)
    end, str)
  end

  # summary Buttonをクリックする
  # param position 対象画面内の上から数えて何番目のButtonか 1から開始
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def click_from_view_to_button(position, wait_time = @wait_time)
    Logger::debug(self, __method__, position)
    click(get_element(wait_time) do
      selector_from_view_button(position)
    end)
    screen_shot
  end

  # summary 対象Elementを左から右方向へスワイプする Elementはidで検索する
  # param id Viewに設定されているid
  # param wait_time 対象要素を発見するまでの待ち時間 default 30
  def swipe_to_right_from_id(id, wait_time = @wait_time)
    Logger::debug(self, __method__, id)
    swipe_to_right(get_element(wait_time) do
      selector_from_id(id)
    end)
    screen_shot
  end

  # summary 指定された座標をタップする。
  # param x x座標 default 中央
  # param y y座標 default 中央
  def click_from_x_y(x = @driver.window_size.width / 2, y = @driver.window_size.height / 2)
    Logger::debug(self, __method__, x, y)
    TouchAction.new(@driver).tap(x: x, y: y).perform
    screen_shot
  end

```

##シナリオ作成
```hogescenario.rb
require_relative "./hogepium.rb"

h = Hogepium.new
h.start_driver
# ほげアイコンをタップ
h.click_from_id("hoge_id")
# 検索フォームに検索ワードを入力
h.input_from_id_to_edittext("search", "検索ワード")
# 検索ボタン押下
h.click_from_id("search_button")
# 検索結果ListViewから任意のTextViewをタップ
h.click_from_textview_in_text("目的行")
# 画面遷移後のページの中程にあるアイコンをクリック
h.click_from_id_scroll("confirm_icon")
```

##メモ
・automationName: "Appium"にすると非常に動きが遅くなるので@wait_timeは30ぐらいがちょうどいいと思う
・ドライバは明示的にquitしなくても勝手にquitされる
・色々やっているうちにautomationNameにuiautomator2を指定するとエラーが発生し動作しなくなりましたが対処方法として下記2コマンドを叩くと良いようです

```
adb uninstall io.appium.uiautomator2.server
adb uninstall io.appium.uiautomator2.server.test
```

おわり