DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

iOSシミュレータでのUIテストの様子を録画してみよう

モバイル 自動化/自動テスト Advent Calendar 2017 14日目の記事です。

はじめまして、SWETグループの加瀬です(@Kesin11
12/05に行われたiOS Test Night #6ではLT枠で発表させて頂きました。

今回は発表で紹介したrecordVideoについての補足と、UIテストを録画する方法を改めて紹介したいと思います。

以後の内容についての動作確認は以下の環境で行いました。

  • Xcode 9.1
  • Ruby 2.3.4
  • Appium 1.7.1

recordVideoとは

xcrun simctl ioに含まれるiOSシミュレータの画面を録画するツールです。 XcodeのリリースノートによるとXcode 8.2より追加された機能です。

使い方はシンプルで、iOSシミュレータを立ち上げてから以下のコマンドを実行するだけです。

xcrun simctl io booted recordVideo ./test.mov

実行するとプロセスが立ち上がり、録画が開始されます。録画の終了はctrl+cです。

上記のコマンド中のbootedは「起動しているシミュレータ」という意味になります。以下のようにシミュレータのUDIDを指定することも可能です。

xcrun simctl io "FDF18984-04A9-4B9A-AD2C-E323152DFD03" recordVideo ./test2.mov

指定しているUDIDは自分の環境でのiOS 11.1、iPhone 5sのシミュレータのものです。

もう少し詳しいオプションなどはxcrun simctl io --helpで確認することができます。

recordVideoでUIテストの様子を録画する

recordVideoを使ってiOSシミュレータの画面を録画することができたので、次はUIテストの開始と終了に合わせて自動的に録画の開始と終了を実行させてみましょう。

今回は以下のシェルスクリプトを作成して簡単に制御してみました。

recorder.sh

# recordVideoをバックグラウンドで実行
xcrun simctl io booted recordVideo screenshots/test.mov &

# プロセスIDを保存
PID=`echo $!`

# テスト実行
# この例ではAppium + RSpectでテストを実行しています
bundle exec rspec spec/scenario_test.rb

# バックグラウンドのrecordVideoにSIGINTシグナルを送信
kill -2 $PID

これだけでテストの起動と終了に合わせて録画ができるようになりました。

Appium + RSpecと組み合わせる

テストに合わせて録画することはできましたが、以下の点をまだ改良できそうです

  • 録画するiOSシミュレータの指定がbootedなので複数のシミュレータが起動している場合は選択できない。UDIDを指定する場合は調べるのが大変
  • UIテスト全体が録画されるため、失敗したテストケースが動画のどの部分か調べることが大変

これらの問題を解決するためAppium + RSpecでのテストに組み込むためのコードをRubyで書いてみました。

recorder.rb

require 'open3'
require 'json'

class Recorder
  attr_reader :udid, :pid, :file_path

  def initialize(driver)
    @udid = _get_udid(driver)
  end

  def _get_udid(driver)
    device_name = driver.caps[:deviceName]
    platform_version = driver.caps[:platformVersion]

    # appiumが実行しているシミュレータのUDIDを取得
    stdout, _stderr, _status = Open3.capture3('xcrun simctl list --json devices')
    json = JSON.parse(stdout)
    booted_device = json.dig('devices', "iOS #{platform_version}").find do |device|
      device['state'] == 'Booted' && device['name'].match(device_name)
    end

    booted_device['udid']
  end

  def start(file_path)
    raise 'UDID is null' unless @udid
    raise 'Already started recording' if @pid

    # バックグラウンドで録画を開始
    @pid = spawn("xcrun simctl io #{@udid} recordVideo #{file_path}", out: '/dev/null', err: '/dev/null')
    @file_path = file_path
    Process.detach(@pid)
  end

  def stop
    raise 'Any recording process started' unless @pid

    # 録画終了
    killed_process_num = Process.kill('SIGINT', @pid)
    raise "Kill pid: #{@pid} did not end correctly" unless killed_process_num.positive?

    # たまに終了に時間がかかる場合があるので待つ。既に終了している場合はエラーになるのでrescueで無視する
    begin
      Process.waitpid(@pid)
    rescue Errno::ECHILD
    end

    @pid = nil
  end

  def remove_video
    raise 'file_path is null' unless @file_path

    File.delete(@file_path)
    @file_path = nil
  end
end

RecorderクラスはrecordVideoを扱いやすくするためのクラスです。start()とstop()で録画をコントロールするのと、Appiumのdriverから録画するシミュレータを特定しています。

シミュレータの特定はAppiumのcapabilityとインストール済みのシミュレータ情報を表示するxcrun simctl listを組み合わせて実現しています。

まず、driverのcapabilityからdeviceName(端末名)とplatformVersion(iOSのバージョン)を取り出します。
xcrun simctl listの実行結果にはシミュレータのUDIDが含まれているので、先程のdeviceNameとplatformVersionと照合することによりUDIDを得ることができます。

次はRecorderクラスを実際に使用するコードです。RSpecを使う場合には共通処理をヘルパーとしてテスト本体とは分離することが多いと思いますので、そこに組み込みます。

spec_helper.rb

capability = {
  caps: {
    # iPhone 5s, iOS 11.1のシミュレータの場合は以下を設定
    platformVersion: '11.1',
    deviceName:      'iPhone 5s',

    # その他の設定は環境に応じて設定してください
    app:             APP_PATH,
    platformName:    'iOS',
    automationName:  'XCUITest',
  },
  appium_lib: {
    wait: 60
  }
}

RSpec.configure do |config|
  config.before(:each) do |example|
    @driver = Appium::Driver.new(capability)
    @driver.start_driver

    @recorder = Recorder.new(@driver)
    @recorder.start("#{ROOT}/screenshots/#{example.description}.mov")
  end

  config.after(:each) do |example|
    @recorder.stop

    # テストが通った場合は録画を消す(=失敗したものは残る)
    @recorder.remove_video unless example.exception

    @driver.driver_quit
  end
end

RSpecはRSpec.configureでbefore(:each)とafter(:each)に処理を仕込むことが可能なのでこれを利用しています。
before(:each)で録画を開始し、after(:each)で録画を終了することでテストケース毎に録画を分割しています。
さらにテストケースが無事に成功した場合には動画を削除することで、失敗したテストケースの動画だけがscreenshot/のディレクトリに最終的に残ります。

必要な処理はこれだけで、実際のテストケースを書くファイルには対応は不要です。

デモ

録画した動画のデモです。実際のUIテストはこの前後に別のテストケースが実行されており、失敗したこのテストケースの部分だけ動画が保存されます。

f:id:swet-blog:20171205133146g:plain:w300
録画したUIテストの様子

ちなみにこちらのデモはiPhone 5sのシミュレータで録画したものであり、25秒の動画で4.5MBのサイズでした。

まとめ

今回はiOS Test Night #6で発表したLTの補足という形でrecordVideoの紹介と、UIテストの様子を録画するTipsを紹介させて頂きました。

今回はAppium + RSpecでのUIテストと組み合わせてみましたが、recordVideo自体はRubyやAppiumに依存するツールではないため他にも活用方法が考えられると思います。
ぜひ色々試してみてください。