メルペイiOSでのAppium活用事例

こんにちは、メルペイiOSチームの@hitsuです。この記事は、Merpay Tech Openness Month 2021 の17日目の記事です。

メルペイiOSチームが日々の開発でテスト自動化の課題を解決するため、Appiumというオープンソースのテスト自動化フレームワークを導入して、更にAppiumに足りない機能を追加しました。他の開発チームにも同じような課題があるかもしれません。この記事で我々の解決方法を紹介させていただき、ご参考になれば幸いです。

背景

メルペイiOSチームで日々開発中にテストする時、いくつかの課題があります。

  • ユーザーのステータス、デバイスとiOSバージョンによって、画面一覧を確認したい
  • 改修した画面の差分を確認したい
  • アニメーションを確認したい
  • テストするとき、既存のアプリに依存したくない
  • QA工数を削減したい
  • エンジニアだけではなく、QAチームやデザインチームなど、誰でも簡単に使えるようにしたい

上記の課題を解決するため、Appiumを導入しました。しかし、Appiumだけで全ての課題を解決できないので、我々がいろいろな改善をしました。

Appiumで解決できる課題 我々が改善して、解決できた課題
・既存のアプリに依存しない
・画面スクリーンショットが撮れる
・アニメーションの動画が撮れる
・使い方を改善して、誰でも簡単に使えるようにした
 ・テストコードを書かなくてもSlackでどの端末のどんなテストを行うかを指定できる
 ・Slack botのコマンドを叩くことでSlackに画面キャプチャが流れる
 ・各画面は端末ごとに一枚の画像にまとめて確認できる
・改修した画面の差分を見れるようにした

メルペイでの活用の成果物

下記の三つの成果物を作りました。

成果物1:画面一覧

Slackで@slack-bot screenshot list を実行すると、端末ごとに指定した画面が一枚の画像で投稿されます。

例:iPhone 12 Pro Max iOS 14.4

例:iPad Pro (12.9-inch) (3rd generation) iOS 13.7

成果物2:画面の差分

Slackで@slack-bot screenshot diff branch-a branch-b を実行すると、それぞれのブランチの差分が投稿されます。緑の枠で差分があるところが示されます。因みに、1pxの差分も検知できるので、人間の目より精度が高いです。

成果物3:画面遷移或いはアニメーションの動画

Slackで@slack-bot screenshot recordを実行すると、動画が投稿されます

端末リスト、画面操作テストケースの設定について

簡単に使えるように、全ての設定は二つのYAMLファイルに纏めています。SlackにYAMLファイルを投稿したら、設定が反映できます。一つ目のYAMLファイルでデバイスとiOSバージョンを指定します。二つ目のYAMLファイルで確認したいユーザーステータスと画面操作を指定します。

デバイス、iOSバージョンを指定

新しいデバイスが発売されても、この方法では1行追加することで、新しいデバイスがテストできます。

"iPhone 12 Pro Max": ['14.5']
"iPhone Xʀ": ['14.5', '13.7']
"iPhone 7": ['13.7']
"iPhone SE (1st generation)": ['13.7']
"iPad Pro (12.9-inch) (3rd generation)": ['14.5','13.7']
"iPad Pro (10.5-inch)": ['13.7']

画面操作テストケースを指定

ユーザーの切り替え、ボタンのクリック、画面のスクロールなどの操作が指定できます。

cases:
  - description: 
      email: 'test-id-1@xxxx.com'
      password: '******'
      endpoint:
        - sleep: '3'
        - tap: 'Merpay'
  - description:
      email: 'test-id-2@xxxx.com'
      password: '******'
      endpoint:
        - sleep: '3'
        - tap: 'My Page'
        - scroll: 'down'

反映する方法

投稿したYAMLファイルのSlackスレのタイムスタンプをコマンドのパラメタに入れて、実行したら、設定が反映されます。

システム構成

基本的な全体構成は以下図のようになっています。 ここでは

  • Slackで端末とテストケースを設定
  • Google App EngineでSlackコマンドを転送
  • CircleCIでAppiumとFastlaneを稼働
  • Fastlaneでテストケースを実行
  • Appiumでテストを行い

前提でシーケンスフローを説明します。

  • Slackで端末とテストケースのYAMLファイルを投稿して、投稿したメッセージのタイムスタンプを取得して、SlackのBotを呼び出し、BotでGCPのApp Engineを起動
  • GCPのApp Engineでコマンドを解析して、CircleCIへメッセージのタイムスタンプとコマンドタイプを転送
  • CircleCIでAppium環境を構築して、Appiumを起動
  • CircleCIでFastlaneを実行して、FastlaneでRubyスクリプトを起動して、メッセージのタイムスタンプでSlackからYAMLファイルを取得
  • Fastlaneでコマンドタイプによって、それぞれのRubyスクリプトを起動して、Appiumをコントロール
  • AppiumでXcodeのシミュレータを起動して、テストを行い、スクリーンショット、動画などのテスト結果を保存
  • Rubyスクリプトでテスト結果を使って、画像合成、差分、圧縮などを再度処理して、Slackへ投稿

システムの構築

Slack Bot、Google App Engine、CircleCI、Appiumについては、本や説明記事がたくさんあるので、この記事では主にAppiumを改善したところを紹介します。

CircleCIでAppium環境の構築

AppiumはRuby、Python、Javaなど、様々な言語をサポートしていますが、FastlaneがRuby製なので、構築の時、RubyとRSpecを選びました。各設定ファイルを下記のように設定しています。

1. Makefileの設定

install_appium:
        brew install ffmpeg # 動画を撮りたいなら、インストールが必要
        brew install --cask chromedriver
        yarn global add appium

2.circleci/config.ymlの設定

appium_screenshot_test:
    executor: macos
    steps:
      - install_dependencies
      - run:
          name: Install appium
          command: make install_appium
      - run:
          name: Run appium
          command: appium
          background: true
      - run:
          name: Run screenshot list by appium
          command: bundle exec fastlane screenshot_test
          no_output_timeout: 6h

CircleCIでiOSのバージョンを指定したい場合

      - run:
          name: Install iOS 12.4
          command: xcversion simulators --install='iOS 12.4'

3.Fastlaneの設定

    private_lane :screenshot_list do |options|
      xcodebuild(
        scheme: 'MerpaySample',
        build: true,
        derivedDataPath: 'appium/apps/list',
        sdk: 'iphonesimulator',
        arch: 'x86_64'
      )

      sh("cd .. && bundle exec rspec appium/spec/list_spec.rb")
    end

テストケースの設計

元々Appiumを使う時、エンジニアがソースコードを書く必要があります。テストケースを設計して、YAMLを設定することでソースコードを書かなくても、誰でも使えるように改善しました。

cases:
 - description:
     email: 'test-id-1@xxxx.com' // テストユーザーID
     password: '******'
     endpoint: // 画面操作を指定
       - sleep: '3'
       - tap: 'Merpay'
         index: '1'
       - scroll: 'down'
       - input: 'Button'
         text: 'Hello world!'
  • Merpayのユーザーには色々なステータスがあります。例えば、銀行を接続していない・接続済み、スマート払いを申請していない・申請済み、バーチャルカード未利用・利用中などがあります。開発の時にはここで各ステータスのユーザーIDを指定でき、それぞれのステータスの画面を見れるためとても便利です。
  • Appiumはいろいろな画面操作ができますが、メルペイで一番よく使っている操作を纏めました。
    • tap: タップしたい場合に、ボタンNameを設定する。例えば、コード決済を設定したら、テストを実行する時、コード決済ボタンがタップされて、QRコード決済画面へ遷移します。
    • index: ボタンNameが重複している場合に、indexで設定できます
    • scroll: 画面をスクロールしたい場合に、upかdownかが指定できます
    • sleep:一定時間処理を待ちます
    • input: 文字列などを入力できます。

下記はテストケースを解析して、Appiumで画面を操作するソースコードです

   def actions driver, endpoints
    for endpoint in endpoints
      if endpoint.has_key?("tap")
        if endpoint.has_key?("index")
          driver.find_elements(:accessibility_id, endpoint["tap"])[endpoint["index"].to_i].click
        elsif
          driver.find_element(:accessibility_id, endpoint["tap"]).click
        end
      elsif endpoint.has_key?("scroll")
          driver.execute_script("mobile: scroll", { "direction" => endpoint["scroll"]})
      elsif endpoint.has_key?("input")
        element = driver.find_element(:accessibility_id, endpoint["input"])
        element.click
        element.send_keys(endpoint["text"])
      end
      endpoint.has_key?("sleep") ? sleep(endpoint["sleep"].to_i) : sleep(2)
    end
  end

画面一覧表示方の改善

元々Appiumが1画面で1枚しか写真を撮れないので、確認する時、見づらいです。端末ごとに指定した複数画面が一枚の画像になるように改善しました。

def compose device, version, spec
  images = Array.new(spec.size)
  width = 0
  height = 0
  padding = 5
  spec.each_with_index do |sp, i|
    file = ...
    img = ChunkyPNG::Image.from_file(file)
    width = width > img.width ? width : img.width
    height = height > img.height ? height : img.height
    images[i] = img
  end

  width += padding
  image = ChunkyPNG::Image.new(width*spec.size, height, ChunkyPNG::Color::BLACK, {})
  images.each_with_index do |img, i|
    next if img.nil?
    image.compose!(img, width*i, 0)
  end

  image.save(file, :fast_rgba)
end

Chunky_pngというRubyの画像ライブラリを使って、端末ごとに複数画面を一枚画像に合成しました。

画面差分を見つける機能

Appiumには差分機能がないので、違う開発ブランチの画面差分を見つける機能を作成しました。

def diff_image driver, spec
  images = [ChunkyPNG::Image.from_file("branch-a.png"), ChunkyPNG::Image.from_file("branch-b.png")]
  diff = []
  status_bar_height = images.first.height * driver.find_element(:class_name, 'XCUIElementTypeNavigationBar').location.y / driver.manage.window.size.height

  images.first.height.times do |y|
    next if y < status_bar_height
    images.first.row(y).each_with_index do |pixel, x|
      diff << [x,y] unless pixel == images.last[x,y]
    end
  end

  return if diff.count == 0

  x, y = diff.map{ |xy| xy[0] }, diff.map{ |xy| xy[1] }
  for i in 0..5
    images.last.rect(x.min-i, y.min-i, x.max+i, y.max+i, ChunkyPNG::Color.rgb(0,255,0), ChunkyPNG::Color::TRANSPARENT)
  end

  image = ChunkyPNG::Image.new(images.first.width+images.last.width+1, images.first.height, ChunkyPNG::Color::BLACK, {})
  image.compose!(images.first, 0, 0)
  image.compose!(images.last, images.first.width+1, 0)

  image.save("diff.png", :fast_rgba)
end

こちらもChunky_pngを使って、実装しました。branch-aとbranch-bの全て画面の画像を撮って、chunky_pngで同じ画面の画像差分を見つけ、差分がある領域を緑の枠で示します。iOSはstatus barのところに時間が表示されており、時間が毎秒変わるので、status barの差分を避けました。

動画のサイズの指定

Slackの仕様で動画サイズは1Gまでです。テストケースの中にサイズを指定しないと、アップロードできない場合があります。例:video_scale: ‘750:1280’

def record device, version, spec
  @driver = Appium::Driver.new($opts, true).start_driver
  @driver.start_recording_screen video_type: 'h264', time_limit: '1800', video_scale: spec["videoScale"]
  ...
  base64_str = @driver.stop_recording_screen
  File.open(video_name, "wb") do |f|
    f.write(Base64.decode64(base64_str))
  end
  @driver.quit
end

おわりに

今回メルペイiOSでのAppium活用事例を紹介しました。開発やQAでテスト自動化の課題を解決したい場合、ご参考になれば幸いです。

iOSチームメンバーである@takeshiが一部機能を改善してくれました。@celiaがCircleCI環境を整備してくれました。@kenmazがSlack Botを導入してくれました。@kuがログインまわりを改善してくれました。皆さんありがとうございます。

明日の記事は @iwata の「[仮] priorityを指定して Cloud Spanner の DML を実行する」になります。引き続き Merpay Tech Openness Month 2021 をお楽しみください。