2013/02/19

シミュレータ上でのiOSアプリのGUIテストをSikuliで自動化する

SikuliではJUnitフレームワークを利用したGUIテストを行うことができますので、iOSシミュレータでのGUIテストを試しに書いてみました。

なお、Sikuliスクリプトの簡単な使いかたについては、前回の記事「SikuliスクリプトでのUI操作自動化を試してみる」を参照してください。

ユニットテストの書式

Sikuliでのユニットテストの標準書式を次に示します。

よくあるユニットテスト風ですが、暗黙に外側でクラス宣言していると思ってよいみたいで、クラス宣言は不要です。

def setUp(self):
    // テスト開始前処理

def tearDown(self):
    // テスト終了後処理

def testA(self):
    // テストA

def testB(self):
    // テストB

IDEにこれを記入した後に、cmd+Uを押してUnitTestビューを表示させます。

そして、Runをクリックしてテストを試しに実行してみましょう。

実行前にファイルを保存するように要求されますので、保存してください。

実行後に、Testsタブを開くと、テストの実行結果が一覧表示され、2つのテストが正常に終了したことがわかります。

テスト内でのアプリケーション操作

まずは、テストごとにiOSシミュレータの起動と終了を行ってみましょう。

テスト内でのアプリケーションの起動や終了などは、Appクラスで行うことができます。

例えば、iOSシミュレータの起動は次のようにします。

APP_PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator"
app = App(APP_PATH)
app.open()

終了は、

app.close()

これらを踏まえて、テストスクリプトのsetUptearDownは次のようになりました。

APP_PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator"
app = App(APP_PATH)

def setUp(self):
    self.app.open()

def tearDown(self):
    self.app.close()

Sikuliスクリプトでは、グローバル変数をself経由でアクセスできるようになるようなので、それを利用しています。

ただし、これを実行させてもシミュレータが一瞬も表示されません。 アプリケーションの起動を待っていないからです。

アプリケーションの起動を待つ

アプリケーションの起動を待つためには正攻法では次のようになります。

def setUp(self):
    win = self.openApp()
    win.wait("INITIAL_WINDOW.png")

iOSシミュレータではハードウェアボタンが見つけられたらいいはずなので、次のようになります。

ただし、こうするとスクリーン全体が検索対象になるので、実行にかなり時間がかかります。

そこで、アプリケーション起動でApp.focusedWindow()でウィンドウが取得できるまで待機するようにしました。

なお、Appクラスのwindow関連のメソッドを利用するにはOS Xのアクセシビリティの設定変更が必要になります。

def openApp(self):
    self.app.open()
    while True:
        sleep(0.1)
        win = self.app.focusedWindow()
        if win: return win
    return None

def setUp(self):
    self.openApp()
    sleep(1)

ウィンドウが表示されてもアニメーションしていたり、中身が表示されなかったりすることがありますので、ウィンドウを取得できてもGUIが利用可能になりそうな状態までwaitで待つようにするとよいでしょう (念のためにsleepするのもよいでしょう)。

ただし、これを実行させると、テストが2つあるときでもシミュレータが1回だけしか表示されません。 アプリケーションの終了を待っていないからです (終了中に次のアプリケーションを起動させようとして失敗している)。

アプリケーションの終了を待つ

アプリケーションの終了を待つには、起動待ちの反対、つまりウィンドウが見つからなくなるまでビジーループで回してやります。

これを`tearDown`で行うようにします。

def closeAppIfRunning(self):
    self.app.close()
    while True:
        sleep(0.1)
        win = self.app.window()
        if not win: return

def tearDown(self):
    self.closeAppIfRunning()

これでテストの回数だけシミュレータが起動/終了するようになりました。

シミュレータ上でアプリケーションを起動させる

iOSシミュレータでは、setUpでさらにアプリケーションを起動させる必要があります。

なお、利用していないので詳細はわかりませんが、WaxSimios-simを利用すれば、このようなことをする必要がなくなりそうですが、どうせなのでSikuliでやってみましょう。

といっても、別によい方法は思いつかなかったので、てきとうに起動させています。

シミュレータ上にアプリケーションが2ページ目にあり、かつ、2ページまでしかない状況だと仮定しています。

起動直後は1ページ目にいるので、ドットをクリックして2ページ目に移動します。 2番目の画像は所定アプリケーションのアイコン、3番目の画像はそのアプリケーションの起動時にあるものだと思ってください。

なお、スクリプトの先頭に次の設定を足しておきましょう。

Settings.MinSimilarity = 1
Settings.MoveMouseDelay = 0

MinSimilarityは画像認識の類似度のグローバル設定で、これを1にすると完全一致になるので、誤判定によるクリックミスが減ります。

MoveMouseDelayはマウスカーソルの移動速度で、0にするとカーソル移動アニメーションがなくなります。

テストを記述する

あとはテストを記述するだけです。ここは各アプリケーションごとに異なるので詳細は説明しません。

次の例ではアクションボタンをタップして、アクションシートが出るのを待ち、Allボタンをタップして、アイテムにチェックされているのを確認してから、削除ボタンをタップするのを自動化しています。

コマンドライン実行

GUIを利用せずにコマンドラインでも実行できます。ただし、次の点に注意。

  • .sikuliは絶対パス指定でなければならない。相対パスしていだとSikuli-IDE.appがあるディレクトリだと勘違いしてしまう
  • .sikuliである必要がある。実行形式(.skl)はだめ。

実行例は次の通り。

> /Volumes/Sikuli-r930-osx-10.6/Sikuli-IDE.app/Contents/MacOS/JavaApplicationStub64 -t $HOME/Documents/my_unittest.sikuli
[info] Sikuli vision engine loaded.
[info] Mac OS X utilities loaded.
[info] VDictProxy loaded.
.[log] App.open /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator(0)
[log] openApp: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator"
[log] CLICK on (392,1519)
[log] CLICK on (131,872)
[log] App.close /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator(0)
[log] closeApp: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator"
[log] closeApp: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator

Time: 4.103

OK (1 test)

IDEでのテストスクリプトのデバッグ

テストが失敗ではなく、エラーになっている場合にはTest Traceタブ内を確認してください。

下の例ではsetUp内で、グローバルなcloseAppIfRunningを呼ぼうとしているのでエラーになっているのがわかります。

フルソース

最後に、実行したスクリプトのフルソースを載せておきます。

Settings.MinSimilarity = 1
Settings.MoveMouseDelay = 0

APP_PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/Applications/iPhone Simulator.app/Contents/MacOS/iPhone Simulator"
app = App(APP_PATH)

def openApp(self):
    self.app.open()
    while True:
        sleep(0.1)
        win = self.app.focusedWindow()
        if win: return win
    return None

def closeAppIfRunning(self):
    self.app.close()
    while True:
        sleep(0.1)
        win = self.app.window()
        if not win: return

def setUp(self):
    self.closeAppIfRunning()
    win = self.openApp()
    win.wait("1360768586802.png")
    win.click(win.getLastMatch())
    win.wait("1360768793179.png")
    win.click(win.getLastMatch())
    win.wait("1360930097299.png")

def tearDown(self):
    self.closeAppIfRunning()

def testInitWindow(self):
    win = self.openApp()
    win.click("1360930097299-1.png")
    win.wait("1361028787371.png")
    win.click("1361029083031.png")
    self.assertNotNull(win.exists("1361028653380.png"))
    win.click("1361028837516.png")

まとめ

Sikuliで、iOSシミュレータのGUIテストを実行する方法を紹介しました。

補足: アクセシビリティ設定変更

Appクラスのwindow関連のメソッドを利用するにはアクセシビリティの設定変更が必要になります。

必要なときは、Sikuliからダイアログで設定を起動しろという指示があります。

設定を起動すると、さらに指示があります。

アクセシビリティで次のチェックボックスをチェック状態にしてダイアログのOKをクリックしてください。

関連リンク

2 件のコメント:

  1. 教えてください。 
    Windowsのせいかわかりませんが、表示のメニュー中に
    UniteTestのウインドウが表示されないのですが
    OS依存なのでしょうか?もしくは表示させる起動方法等あるのでしょうか?

    返信削除
  2. 最近Sikuliを使っていないので細かいことは把握していませんが、
    バグが多かったみたいで、1.0から1.1に上がったときにテストの機能自体がIDEから削られているみたいです。

    https://answers.launchpad.net/sikuli/+question/268462

    返信削除

注: コメントを投稿できるのは、このブログのメンバーだけです。