2011/08/30

Apple Instruments Automation with Titanium Mobile

Apple謹製のUIテストツール Instruments Automationは、Javascriptで書かれたテストケースを実行してUIテストするツールです。
[iPhone] JavaScriptを使ってUIのデバッグを自動化してみた
[iPhoneプログラミング]iPhone SDK 4からの新機能UI Automationの使い方
などで、Instruments Automationを使用して、XcodeのiOS用プロジェクトをUIテストする例が述べられています。
また、
titanium/UIをテストする(iPhone編)
で、Instruments Automationを使用して、Titanium MobileのプロジェクトをUIテストする例が述べられています。
Instruments Automationは、InterfaceBuilderを使ったプロジェクトを前提としているらしく、シンプルなnon IBのプロジェクトでは、アプリケーションに含まれるUI 要素の名前を使った参照ができないようです。
試しにUI要素の階層構造を取得するために
UIATarget.localTarget().logElementTree();
で、Titanium Mobileプロジェクトの情報を取得してみると、


--logElementTree()
2) UIAApplication [name:Sample value:(null) rect:{{x:0, y:20}, {width:320, height:460}}]
3) UIAWindow [name:(null) value:(null) rect:{{x:0, y:0}, {width:320, height:480}}]
4) UIAStaticText [name:capability value:(null) rect:{{x:20, y:70}, {width:100, height:50}}]
4) UIAStaticText [name:1 value:(null) rect:{{x:135, y:70}, {width:50, height:50}}]
4) UIAStaticText [name:state value:(null) rect:{{x:20, y:170}, {width:100, height:50}}]
4) UIAStaticText [name:0 value:(null) rect:{{x:135, y:170}, {width:50, height:50}}]
4) UIASwitch [name:(null) value:0 rect:{{x:113, y:270}, {width:94, height:27}}]

のように、nameの値が(null)になっているものがあります。
多くの場合、UI要素にtitleプロパティがないもので、nameの値が(null)になるようです。
例えば、Ti.UI.windowならば、titleプロパティを設定すれば、nameによって参照できるようになります。それに対して、Ti.UI.Switchにはtitleプロパティがないようです。そもそもTi.UI.Switchの実体であるUISwitchに該当するプロパティが存在していません。
勢い、nameによる参照をあきらめて、
var switchs = view.switches();
switchs[0].setValue(true);
のように、インデックスによる参照に向かってしまいますが、おそらくこの方式では、Titanium Mobileプロジェクトのコード内で、UI要素をaddする順番を変えたりすると、テストがテストにならない結果になってしまいます。

ではどうすればよいのか?
Instruments AutomationはIBで設定されるAccessibility Labelをnameとして使用しています。non IBのプロジェクトをInstruments Automationでテストするには、テストしたいUI要素に対して、自前でsetAccessibilityLabelを使ってラベルを設定してやる必要がある訳です。


(ここから先は、Titanium Mobile SDKにある元のObjective-Cのソースファイルに手を加える形になりますので、On Your Riskでお願いします)


Ti.UI.Switchを例にとると、Titanium Mobile SDKディレクトリ(/Library/Application Support/Titanium/ か ~/Library/Application Support/Titanium/)以下のmobilesdk/osx/<sdk version>/iphone/Classes内のTiUISwitch.mが対応するソースファイルになります。
@implementation TiUISwitch
@end
の実装部分に以下のメソッドを追加します。


-(void)setTitle_:(id)value
{
UISwitch * ourSwitch = [self switchView];
[ourSwitch setIsAccessibilityElement:YES];
[ourSwitch setAccessibilityLabel:[TiUtils stringValue:value]];
}


これで、Ti.UI.Switchに対して設定可能なtitleプロパティが追加されたことになります。


Titanium側のJavascriptでは、
var sw1 = Ti.UI.createSwitch({
value:false,
title:'testSwitch',


height:'60dp',
width:'150dp',
top:'250dp'
});
のように、titleプロパティの値を追加してcreateするようにします。

これによって、Ti.UI.Switchにもnameをつけることができました。

Ti.UI.Switch以外のUI要素についても同様の追加が可能と思われますので、必要な部分に適宜追加するのがいいと思います。

(TiStudioで既に作成中のプロジェクトについて、上の結果を反映させるには、TiStudioで[project > clean...]でクリーンした上で、プロジェクトパスにあるbuild/iphone/Classes以下のファイルを削除しておくのがよいかもしれません)



UI Automation Reference Collection





2011/08/21

nativedriver for iOS で実機 UIテスト

1.プロジェクト設定の修正と実機実行
現在の nativedriver の配布では、テスト対象アプリ SampleApp の Automation ターゲットは、実機用にビルドできないようです。依存関係にある nativedriver.xcodeproj と CocoaHTTPServerLibrary.xcodeproj が、SampleApp のプロジェクト設定と齟齬があるためです。ビルド可能な設定を以下に示します。
nativedriver.xcodeprojの設定



CocoaHTTPServerLibrary.xcodeprojの設定



 要点は、SampleAppプロジェクトの Architectures, Base SDK, Build Active Architecture Onlyの箇所と、nativedriver、CocoaHTTPServerLibraryのそれらが食い違っているので、SampleAppプロジェクトと同じにします。
これで、ビルドに必要な設定は、完了しているので、Code Signの設定等が正しくされていれば、実機デバイスへ転送してSampleAppを実行できます。

2.テストの修正と実行
SampleApp用のテストケースのEclipseプロジェクトを前回作りましたが、実機を対象にUIテストする場合には、1箇所修正する必要があります。nativedriver は、テスト対象アプリを HTTP サーバとして、テストケースを含むテストアプリが HTTP クライアントとして接続して、UIテストを実行します。テスト対象がシミュレータ上のアプリの場合、nativedriver は、同一マシン上にあるため、ローカル・ループバック・アドレス(127.0.0.1)を使って、接続するようになっています。このとき、NativeDriverTest.javaの nativedriver の初期化は、以下のようにデフォルト値で行われています。 

public void testNativeDriver() throws Exception {
WebDriver driver = new IosNativeDriver();

 この箇所にテスト対象を転送した実機のIPアドレスを設定することになります。 nativedriver は、ポート3001を使用し、HTTPサーバのディレクトリのエントリはhubとなっているので、実機IPアドレスが 192.168.0.64 の場合、以下のようになります。

public void testNativeDriver() throws Exception {
WebDriver driver = new IosNativeDriver("http://192.168.0.64:3001/hub");
 シミュレータでのテストと同様に、実機上のテスト対象アプリ SampleApp (Automationというアプリ名になっている)を起動しておいて、テストをJUnit Test として実行すれば、UIテストが実行されます。

注)実機がLAN上にないとnativedriverから接続できませんので、iPhoneをWiFi接続しておく必要があります。また、そのときのIPアドレスは、iPhone上で設定アプリから容易に知ることができます。

2011/08/20

nativedriver for iOS を使ってみる

nativedriver は、Google東京オフィスで開発している UI テストツールです。  
Android,iOS,Windowsなどのマルチプラットフォームで動くUIテストツールを目指しています。最近(8/18)iOS版が加わったので、試してみました。
簡単な解説とデモムービーは、UI テストツール「NativeDriver」の iOS 版をリリースしましたで見ることができます。 基本コンセプトは、ここにある"NativeDriver introduction" に説明があります。


 テスト対象のアプリ内に HTTP サーバをおき、テストツールがクライアントとしてコネクトして JUnit のテストコードを実行することで、UIテストをする構造になっているようです。アプリ内に HTTP サーバを実現するためのライブラリが NativeDriver のコアになる部分で、テスト対象のアプリをビルドするときにスタティックリンクする形になるので、配布用のアプリとは別に UIテスト用のアプリを別のターゲットとして作成することになります。この NativeDriver のライブラリはプライベートAPIを使っているので、配布用のアプリに含めると、Apple の審査を通らないことになるので、注意が必要です。
 まず、最初の手がかりとして、GettingStartedIOS に従って、ライブラリとサンプルアプリを作りテストを実行してみるのが理解の早道でしょう。開発環境は以下の通りです。
  • Xcode 4 
  • Eclipse version 3.5 以降 (Eclipse IDE for Java Developers が必要) - download
  • Ant - download
  • JDK 1.6 以降 - download

0.ソースコードの入手
 Google Codeからソースコードを入手するためにsvnが必要になるので、インストールしておく必要があります。ここではコマンドライン版のsvnを使います。  適当なディレクトリを作り
$ svn checkout https://nativedriver.googlecode.com/svn/trunk nativedriver
で、リポジトリをチェックアウトすると、nativedriver 以下のディレクトリにソースコードが次のような構成で展開されます (Android用はソースコードは省略しています)。
nativedriver 

|__iphone 
| |__Classes (サーバコード) 
| |__Client (クライアントコード) 
| |__NativeDriver.xcodeproj (サーバのXcode プロジェクト) 
| |__SampleApp (テスト用のサンプルアプリ) 
| |__Tests (サーバコードの単体テスト) 
| |__ThirdParty (必要なObjective-C ライブラリ) 
|
|__third_party (必要なJava ライブラリ)


1.SampleAppのビルド/ラン
  nativedriver/iphone/SampleApp/SampleApp.xcodeproj を Xcode で開ます。既にプロジェクトに nativedriver サーバのライブラリ ( nativedriver/iphone/NativeDriver.xcodeproj ) が組み込まれていますので、修正は不要です。ターゲットとして Automation iPhone 4.3 Simulator を選択し、ビルド/ランします。




ビルドが成功すれば、エミュレータが立ち上がり、SampleAppが実行されます。
 テスト実行時にこの SampleApp がテスト対象の nativedriver サーバとなります。



2.クライアントライブラリのビルド
 nativedriver クライアントの機能を実現するためのライブラリ ios-client-standalone.jar をビルドします。 元になる ant プロジェクトは、nativedriver/iphone/Client にあります。このディレクトリに移動して ant を実行すれば、ビルドがはじまります。
$ cd nativedriver/iphone/Client
$ ant
Buildfile: /Users/<.....>/nativedriver/iphone/Client/build.xml
directory:
[mkdir] Created dir: /Users/<.....>/nativedriver/iphone/Client/class
[mkdir] Created dir: /Users/<.....>/nativedriver/iphone/Client/build
classes-client:
[javac] /Users/<.....>/nativedriver/iphone/Client/build.xml:30: warning: 'includeantruntime' was not set, defaulting to build.sysclasspath=last; set to false for repeatable builds
[javac] Compiling 6 source files to /Users/<.....>/nativedriver/iphone/Client/class
jar-client:
[jar] Building jar: /Users/<.....>/nativedriver/iphone/Client/build/ios-client.jar
standalone-client:
[zip] Building zip: /Users/<.....>/nativedriver/iphone/Client/build/ios-client-standalone.jar
eclipse-references:
[copy] Copying 1 file to /Users/<.....>/nativedriver/iphone/Client/test
compile:
BUILD SUCCESSFUL
Total time: 5 seconds





3.サンプルテストケースのインポート
 SampleApp に対するテストケースは、nativedriver/iphone/Client/test にあります。この中に、テストケースのコードが格納されたディレクトリがあります。また、先ほどビルドされたios-client-standalone.jarがコピーされているはずです。これらを Eclipse に既存のプロジェクトしてインポートしてプロジェクトを作成します。
  1. Eclipse を起動
  2. メニューの File>Import を選択
  3. カテゴリ General の Existing Projects into Workspace を選択し、Next ボタンを押す
  4. チェックボックス Select root directory をチェックして、Browse... ボタンを押す
  5. nativedriver/iphone/Client/test を選択して、Open ボタンを押す
  6. Project: 欄に、ios-test(<ディレクトリ名>) にチェックが入ってるのを確認して、Finishボタンを押す
インポートされると Package Explorer に ios-test というプロジェクトが追加されます。




4.テストの実行
 以上で、nativedriver クライアントを実行して、UIテストする準備が整いました。
プロジェクトに含まれるテストコード NativeDriverTest.java(のクラスNativeDriverTest)をJUnit Testとして実行すると、UIテストが実行されます。

このとき、1.の手順でランしていたSampleApp がエミュレータで実行されていない場合、当然UIテストが正しく行えません。JUnit Testを実行する前に、SampleApp をランしておいてください。
次の画面は、テスト完了時の様子です。

最後に
 とりあえず、SampleApp のUIテストできるようになりましたが、実機での UIテストはどうするのか、あるいは、具体的なテストコードはどう書けばいいかなど、実際に自分のプロジェクトに取り入れる上で知らなければならない点はたくさんあります。それらの点はまた項を改めて述べたいと思います。

2011/08/15

Titanium mobile へのモジュールの読み込み

この記事の結論は、モジュールの読み込み指定には
<modules>
  <module platform="iphone" version="0.1">com.bar.foo</module>
  <module platform="android" version="0.1">com.bar.foo</module>
</modules>
というように、platform属性を書くべきだ、ということです。
これについては、AppceleratorのQ&Aの
Compile fails when having both an iPhone and Android module
で簡単に触れられているだけです。以下に状況も含めて説明します。


iOS用とAndroid用両方のモジュールを作っていると、おかしなエラーに遭遇することがあります。
例えば、module idを”com.bar.foo"とすると、Ti StudioでモジュールのテストアプリをiOS用にビルドすると、次のようなエラーになることがあります。
[DEBUG] Looking for Titanium Module id: com.bar.foo, version: 0.1, platform: <any platform>
[ERROR] Third-party module: com.bar.foo/0.1 missing library at /Library/Application Support/Titanium/modules/android/com.bar.foo/0.1/libcom.bar.foo.a
[ERROR] Error: Traceback (most recent call last):
 File "/Library/Application Support/Titanium/mobilesdk/osx/1.7.2/iphone/builder.py", line 638, in main
    sys.exit(1)
SystemExit: 1
なぜかandroid用のモジュールディレクトリを探しています。そして、当然のことながらそこにはiOS用のモジュールのライブラリファイル(*.a)が存在しないので、エラーとなってしまいます。よく見るとplatform: <any platform>を探そうとしているらしいのですが、どうしてanyであるにもかかわらず、androidで停止してしまうのか怪訝に思います。
このとき、xapp.xmlには
<modules> 
   <module version="0.1">com.bar.foo</module>

</modules>
と指定してあります。
モジュールの開発過程を振り返ってみると、iOS用のモジュールを作り、サンプル用のアプリとインターフェースの調整をして、一通りつじつまの合うようにして、一旦iOS用の開発を中断して、Android用のモジュールの開発に移りました。iOSと同じように一通り完成したので、もういちどiOS用の開発に戻ったときこれが起きました。かなり時間を使ってから、Android用のモジュールの存在に気がつきました。
そうなんです。問題を引き起こしたのは、今まさに作ったばかりのAndroid用のモジュールです。試しに、Android用モジュールをTitaniumディレクトリから削除してみると、
[DEBUG] Looking for Titanium Module id: com.bar.foo, version: 0.1, platform: <any platform>
[INFO] Detected third-party module:com.bar.foo/0.1
となって、うまくビルドできます。 この方法で、iOS用のアプリがビルドできるのですが、ターゲットを変えるたびに、いちいちTitaniumディレクトリの中をいじるのは何かと面倒です。
(iOS用にビルドしているのにandroid用のモジュールディレクトリを探しているのがそもそも間違いなので、iOS用のbuild.pyを修正する方法もあります。しかしこれも何かと面倒です)。
そこで、platform属性の登場です。
<module version="0.1">com.bar.foo</module>
は、any platformのモジュールを意味しています。これをプラットフォームごとに指定するために、 
<module platform="iphone" version="0.1">com.bar.foo</module>
<module platform="android" version="0.1">com.bar.foo</module>
のように、platform属性を指定して、それぞれのプラットフォームだけを探すように指定します。幾分冗長な感じがしますが、現状のTitaniumのビルドシステムでは、最も簡単で安全な方法です。
モジュール開発だけでなく、アプリケーションをビルドする場合の問題なので、iOS、Android両環境向けにアプリケーション開発する場合は、必ずplatform属性を使った方がいいでしょう。


PS
ところで、any platformなんて、いったいどうしてそんなものが必要なのかよくわからないのですが、Javascript Native Moduleと呼ばれるモジュールがany platform向けのモジュールなんじゃないかな、と思っていたんです。しかしそれについてiOS用しかドキュメントされていないし、Android用は作れないようです。iOS用も*.aファイルになってしまうので共通で使える訳ではないようです。どうしてany platformなんて考えになったんですかね?

さて、はじめます

長らく作ったまま放置状態のブログを始めます。
GitHubやGoogle Codeもアカウントをとったまま放置状態でした。
ここでは、主にソフトウエア関係の諸々を書こうと思います。
ときどきGNUのTシャツを着てたりする割には、いままで、積極的に技術情報を公開することをしてきませんでした。主な原因は業務上の問題等々あった訳ですが、そろそろいい時期が来ています。
ということではじめます。