エンドツーエンドの TF テストの例

このチュートリアルでは、「hello world」の Trade Federation(Tradefed あるいは TF)テスト構成を作成し、TF フレームワークの実践的な説明を行います。開発環境から開始して、シンプルな構成を作成し、機能を追加します。

チュートリアルでは、テスト開発プロセスを一連の演習として示します。それぞれが複数のステップで構成されており、構成を作成して段階的に調整する方法を示します。テスト構成を完了するために必要なすべてのサンプルコードが提供され、各演習のタイトルには、そのステップに関係する役割を説明する文字が注記されています。

  • D: 開発者
  • I: インテグレータ
  • R: テストランナー

チュートリアルを完了すると、TF 構成が機能するようになり、TF フレームワークの多くの重要な概念を理解できます。

Trade Federation を設定する

TF 開発環境の設定の詳細については、マシンのセットアップをご覧ください。このチュートリアルの残りの部分では、TF 環境で初期化されたシェルを開いていることを前提としています。

わかりやすくするために、このチュートリアルでは、構成とクラスを TF フレームワークのコアライブラリに追加する方法を説明します。これは、tradefed JAR をコンパイルしてから、その JAR に対してモジュールをコンパイルすることで、ソースツリーの外部でのモジュール開発に拡張できます。

テストクラスを作成する(D)

メッセージを stdout にダンプするだけの hello world テストを作成しましょう。一般に、tradefed テストは IRemoteTest インターフェースを実装します。HelloWorldTest の実装を次に示します。

package com.android.tradefed.example;

import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.invoker.TestInformation;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.ITestInvocationListener;
import com.android.tradefed.testtype.IRemoteTest;

public class HelloWorldTest implements IRemoteTest {
    @Override
    public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException {
        CLog.i("Hello, TF World!");
    }
}

このサンプルコードを <tree>/tools/tradefederation/core/src/com/android/tradefed/example/HelloWorldTest.java に保存し、シェルから変換してリビルドします。

m -jN

上記の例の CLog.i は、出力をコンソールに出力するために使用されます。Trade Federation のロギングの詳細については、ロギング(D、I、R)をご覧ください。

ビルドが成功しない場合は、マシンのセットアップを参照して、手順を見落としていないか確認します。

構成を作成する(I)

Trade Federation テストは、構成(どのテストを実行するか、また他のどのモジュールをどういった順番で実行するかを tradefed に指示する XML ファイル)を作成することで、実行可能ファイルを作成します。

HelloWorldTest の新しい構成を作成しましょう(HelloWorldTest の完全なクラス名に注意します)。

<configuration description="Runs the hello world test">
    <test class="com.android.tradefed.example.HelloWorldTest" />
</configuration>

このデータをローカル ファイル システムの任意の場所(例: /tmp/helloworld.xml)にある helloworld.xml に保存します。TF は構成 XML ファイル(別名 config)を解析し、リフレクションを使用して指定されたクラスを読み込み、インスタンス化し、IRemoteTest にキャストして、run メソッドを呼び出します。

構成を実行する(R)

シェルから tradefed コンソールを起動します。

tradefed.sh

デバイスがホストマシンに接続されており、tradefed に認識されていることを確認します。

tf> list devices
Serial            State      Product   Variant   Build   Battery
004ad9880810a548  Available  mako      mako      JDQ39   100

構成は、run <config> のコンソール コマンドを使用して実行できます。次を試します。

tf> run /tmp/helloworld.xml
05-12 13:19:36 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World!

ターミナルに「Hello, TF World!」という出力が表示されます。

コンソール プロンプトで list invocations または l i を使用すると、コマンドの実行を確認できます。出力は出力されません。コマンドが実行中の場合は、次のように表示されます。

tf >l i
Command Id  Exec Time  Device       State
10          0m:00      [876X00GNG]  running stub on build(s) 'BuildInfo{bid=0, target=stub, serial=876X00GNG}'

クラスパスに構成を追加する(D、I、R)

デプロイの便宜上、構成を tradefed JAR 自体にバンドルすることもできます。Tradefed はクラスパスの config フォルダにあるすべての構成を自動的に認識します。

説明のため、helloworld.xml ファイルを tradefed コアライブラリ(<tree>/tools/tradefederation/core/res/config/example/helloworld.xml)に移動します。tradefed をリビルドし、tradefed コンソールを再起動してから、tradefed にクラスパスから構成のリストを表示するように依頼します。

tf> list configs
[…]
example/helloworld: Runs the hello world test

これで、下記を使用して helloworld 構成を実行できます。

tf> run example/helloworld
05-12 13:21:21 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World!

デバイスを操作する(D、R)

これまでのところ、HelloWorldTest は何も面白いことをしていません。Tradefed の専門は、Android デバイスを使用したテストです。そのため Android デバイスをテストに追加しましょう。

テストは、IRemoteTest#run メソッドが呼び出されたときにフレームワークによって提供される TestInformation を使用して、Android デバイスへの参照を取得できます。

HelloWorldTest のプリント メッセージを変更して、デバイスのシリアル番号を表示しましょう。

@Override
public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException {
    CLog.i("Hello, TF World! I have device " + testInfo.getDevice().getSerialNumber());
}

次に、tradefed をリビルドし、デバイスのリストを確認します。

tradefed.sh
tf> list devices
Serial            State      Product   Variant   Build   Battery
004ad9880810a548  Available  mako      mako      JDQ39   100

Available としてリストされたシリアル番号をメモします。これが、HelloWorld に割り当てられるデバイスです。

tf> run example/helloworld
05-12 13:26:18 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World! I have device 004ad9880810a548

デバイスのシリアル番号を示す新しいプリント メッセージが表示されます。

テスト結果を送信する(D)

IRemoteTest は、#run メソッドに提供される ITestInvocationListener インスタンスのメソッドを呼び出すことで、結果を報告します。TF フレームワーク自体は、各呼び出しの開始(ITestInvocationListener#invocationStarted 経由)と終了(ITestInvocationListener#invocationEnded 経由)の報告を担当します。

テスト実行は、テストの論理的な集合です。テスト結果を報告するために、IRemoteTest はテスト実行の開始、各テストの開始と終了、テスト実行の終了を報告します。

下記は、1 つの不合格のテスト結果によって HelloWorldTest 実装がどのようになるかを示します。

@Override
public void run(TestInformation testInfo, ITestInvocationListener listener) throws DeviceNotAvailableException {
    CLog.i("Hello, TF World! I have device " + testInfo.getDevice().getSerialNumber());

    TestDescription testId = new TestDescription("com.example.TestClassName", "sampleTest");
    listener.testRunStarted("helloworldrun", 1);
    listener.testStarted(testId);
    listener.testFailed(testId, "oh noes, test failed");
    listener.testEnded(testId, Collections.emptyMap());
    listener.testRunEnded(0, Collections.emptyMap());
}

TF には、独自にゼロから作成する代わりに再利用できるいくつかの IRemoteTest 実装が含まれます。たとえば、InstrumentationTest は、Android デバイス上で Android アプリのテストをリモートで実行し、結果を解析して、その結果を ITestInvocationListener に転送できます。詳細については、テストタイプをご覧ください。

テスト結果を保存する(I)

TF 構成のデフォルトのテストリスナー実装は TextResultReporter であり、呼び出しの結果を stdout にダンプします。説明のため、前のセクションの HelloWorldTest 構成を実行します。

./tradefed.sh
tf> run example/helloworld
04-29 18:25:55 I/TestInvocation: Invocation was started with cmd: /tmp/helloworld.xml
04-29 18:25:55 I/TestInvocation: Starting invocation for 'stub' with '[ BuildInfo{bid=0, target=stub, serial=876X00GNG} on device '876X00GNG']
04-29 18:25:55 I/HelloWorldTest: Hello, TF World! I have device 876X00GNG
04-29 18:25:55 I/InvocationToJUnitResultForwarder: Running helloworldrun: 1 tests
04-29 18:25:55 W/InvocationToJUnitResultForwarder:
Test com.example.TestClassName#sampleTest failed with stack:
 oh noes, test failed
04-29 18:25:55 I/InvocationToJUnitResultForwarder: Run ended in 0 ms

呼び出しの結果を、ファイルなどの別の場所に格納するには、構成内で result_reporter タグを使用して、カスタムの ITestInvocationListener 実装を指定します。

TF には XmlResultReporter リスナーも含まれており、テスト結果は、ant JUnit XML ライターが使用するものと同様の形式で XML ファイルに書き込まれます。構成で result_reporter を指定するには、…/res/config/example/helloworld.xml 構成を編集します。

<configuration description="Runs the hello world test">
    <test class="com.android.tradefed.example.HelloWorldTest" />
    <result_reporter class="com.android.tradefed.result.XmlResultReporter" />
</configuration>

tradefed をリビルドし、hello world サンプルを再実行します。

tf> run example/helloworld
05-16 21:07:07 I/TestInvocation: Starting invocation for target stub on build 0 on device 004ad9880810a548
Hello, TF World! I have device 004ad9880810a548
05-16 21:07:07 I/XmlResultReporter: Saved device_logcat log to /tmp/0/inv_2991649128735283633/device_logcat_6999997036887173857.txt
05-16 21:07:07 I/XmlResultReporter: Saved host_log log to /tmp/0/inv_2991649128735283633/host_log_6307746032218561704.txt
05-16 21:07:07 I/XmlResultReporter: XML test result file generated at /tmp/0/inv_2991649128735283633/test_result_536358148261684076.xml. Total tests 1, Failed 1, Error 0

XML ファイルが生成されたことを示すログメッセージが表示されます。生成されるファイルは次のようになります。

<?xml version='1.0' encoding='UTF-8' ?>
<testsuite name="stub" tests="1" failures="1" errors="0" time="9" timestamp="2011-05-17T04:07:07" hostname="localhost">
  <properties />
  <testcase name="sampleTest" classname="com.example.TestClassName" time="0">
    <failure>oh noes, test failed
    </failure>
  </testcase>
</testsuite>

ITestInvocationListener インターフェースを実装するだけで、独自のカスタム呼び出しリスナーを作成することもできます。

Tradefed は複数の呼び出しリスナーをサポートしているため、テスト結果を複数の独立した宛先に送信できます。これを行うには、構成で単に複数の <result_reporter> タグを指定します。

ロギング機能(D、I、R)

TF のロギング機能には、次の機能が含まれます。

  1. デバイスからログをキャプチャする(別名: デバイス logcat)
  2. ホストマシンで実行されている Trade Federation フレームワークからログを記録する(別名: ホストログ)

TF フレームワークは、割り当てられたデバイスから自動的に logcat をキャプチャし、呼び出しリスナーに送信して処理します。XmlResultReporter は、キャプチャしたデバイス logcat をファイルとして保存します。

TF ホストログは、ddmlib ログクラスの CLog ラッパーを使用して報告されます。HelloWorldTest の、前の System.out.println 呼び出しを CLog 呼び出しに変換しましょう。

@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    CLog.i("Hello, TF World! I have device %s", getDevice().getSerialNumber());

CLog は、String.format と同様に、文字列補間を直接処理します。TF をリビルドして再実行すると、stdout にログメッセージが表示されます。

tf> run example/helloworld
…
05-16 21:30:46 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548
…

デフォルトでは、tradefed はホスト ログメッセージを stdout に出力します。TF には、メッセージをファイルに書き込む FileLogger というログ実装も含まれています。ファイル ロギングを追加するには、FileLogger の完全なクラス名を指定して、logger タグを構成に追加します。

<configuration description="Runs the hello world test">
    <test class="com.android.tradefed.example.HelloWorldTest" />
    <result_reporter class="com.android.tradefed.result.XmlResultReporter" />
    <logger class="com.android.tradefed.log.FileLogger" />
</configuration>

helloworld の例をリビルドして実行します。

tf >run example/helloworld
…
05-16 21:38:21 I/XmlResultReporter: Saved device_logcat log to /tmp/0/inv_6390011618174565918/device_logcat_1302097394309452308.txt
05-16 21:38:21 I/XmlResultReporter: Saved host_log log to /tmp/0/inv_6390011618174565918/host_log_4255420317120216614.txt
…

ログメッセージは、ホストログのパスを示します。表示されたとき、HelloWorldTest ログメッセージが含まれます。

more /tmp/0/inv_6390011618174565918/host_log_4255420317120216614.txt

出力例:

…
05-16 21:38:21 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548

オプションの処理(D、I、R)

TF 構成(別名: 構成オブジェクト)から読み込まれたオブジェクトは、@Option アノテーションを使用してコマンドライン引数からデータを受け取ることもできます。

参加するために、構成オブジェクト クラスは、@Option アノテーションをメンバー フィールドに適用し、一意の名前を付けます。これにより、メンバー フィールド値をコマンドライン オプションで入力できるようになります(また、オプションが構成ヘルプシステムに自動的に追加されます)。

注: すべてのフィールド タイプがサポートされているわけではありません。サポートされているタイプについては、OptionSetter をご覧ください。

@Option を HelloWorldTest に追加しましょう。

@Option(name="my_option",
        shortName='m',
        description="this is the option's help text",
        // always display this option in the default help text
        importance=Importance.ALWAYS)
private String mMyOption = "thisisthedefault";

次に、HelloWorldTest のオプションの値を表示するログメッセージを追加し、正しく受け取ったことを示せるようにします。

@Override
public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
    …
    CLog.logAndDisplay(LogLevel.INFO, "I received option '%s'", mMyOption);

最後に、TF をリビルドして helloworld を実行します。my_option デフォルト値とともにログメッセージが表示されます。

tf> run example/helloworld
…
05-24 18:30:05 I/HelloWorldTest: I received option 'thisisthedefault'

コマンドラインから値を渡す

my_option の値を渡します。その値が入力された my_option が表示されます。

tf> run example/helloworld --my_option foo
…
05-24 18:33:44 I/HelloWorldTest: I received option 'foo'

TF 構成には、@Option フィールドのヘルプテキストを自動的に表示するヘルプシステムも含まれています。試してみると、my_option のヘルプテキストが表示されます。

tf> run example/helloworld --help
Printing help for only the important options. To see help for all options, use the --help-all flag

  cmd_options options:
    --[no-]help          display the help text for the most important/critical options. Default: false.
    --[no-]help-all      display the full help text for all options. Default: false.
    --[no-]loop          keep running continuously. Default: false.

  test options:
    -m, --my_option      this is the option's help text Default: thisisthedefault.

  'file' logger options:
    --log-level-display  the minimum log level to display on stdout. Must be one of verbose, debug, info, warn, error, assert. Default: error.

「重要なオプションのみを出力しています」という内容のメッセージに注目してください。オプションのヘルプを整理するために、TF は Option#importance 属性を使用して、--help が指定されている場合に特定の @Option フィールド ヘルプテキストを表示するかどうかを判断します。--help-all は、重要性に関係なく、常にすべての @Option フィールドのヘルプを表示します。詳細については、Option.Importance をご覧ください。

構成から値を渡す

<option name="" value=""> 要素を追加することで、構成内でオプション値を指定することもできます。helloworld.xml を使用して試します。

<test class="com.android.tradefed.example.HelloWorldTest" >
    <option name="my_option" value="fromxml" />
</test>

helloworld をリビルドして実行すると、次の出力が生成されます。

05-24 20:38:25 I/HelloWorldTest: I received option 'fromxml'

構成のヘルプも更新されて、my_option のデフォルト値が表示されます。

tf> run example/helloworld --help
  test options:
    -m, --my_option      this is the option's help text Default: fromxml.

helloworld 構成に含まれる他の構成オブジェクト(FileLogger など)もオプションを受け入れます。--log-level-display は、stdout に表示されるログをフィルタするため、興味深いオプションです。チュートリアルの前半で、「Hello, TF World! I have device …」というログメッセージが、FileLogger の使用に切り替えた後、表示されなくなったことに気付いたかもしれません。--log-level-display 引数を渡すことで、stdout へのロギングの冗長性を高めることができます。

試してみると、ファイルに記録されるだけでなく、「I have device」というログメッセージが stdout に再び表示されます。

tf> run example/helloworld --log-level-display info
…
05-24 18:53:50 I/HelloWorldTest: Hello, TF World! I have device 004ad9880810a548

これで終わりです

何かに行き詰まっているのであれば、念のため、Trade Federation ソースコードをご覧ください。ドキュメントでは公開されていない有用な情報がたくさんあります。それ以外の問題がある場合は、メッセージの件名に「Trade Federation」を入れて、Android プラットフォーム の Google グループに問い合わせてみてください。