Instrumentationによるユニットテストでは直接ビューを触ってはいけない
ActivityInstrumentationTestCase2クラスなどを使用してActivityをテストする際に、うっかりテスト中で
Button button = (Button)activity.findViewById(R.id.Button01); button.performClick();
などと書いてしまうと
android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views. at android.view.ViewRoot.checkThread(ViewRoot.java:2683) at android.view.ViewRoot.playSoundEffect(ViewRoot.java:2472) at android.view.View.playSoundEffect(View.java:8306) at android.view.View.performClick(View.java:2363) :
と、思いっきり怒られる。
これはUIスレッド以外のスレッドでビューを操作することを禁じているためで、このケースではInstrumentationによるテストはテスト対象のアプリケーションを外側から起動しており、当然UIスレッド外となる。
Java(JFC/Swing)や.NET C#(Windows Forms)等と同様に、シングルスレッドモデルを使用しているAndroid SDKでは当たり前のことなのだが、ついつい忘れがちだ。
どうするのが正しいか?
1. Handlerを使用する
Android SDKではマルチスレッド下でGUIを操作するために、Handlerクラスが用意されている。
private Handler handler = new Handler(); private Activity activity; protected void setUp() throws Exception { super.setUp(); activity = this.getActivity(); } public test_click_button() { handler.post(new Runnable(){ @Override public void run() { Button button = (Button)activity.findViewById(R.id.Button01); button.performClick(); } }); }
Handlerによって、GUIへの操作はUIスレッドと同期される。
なお、Handlerのインスタンスはこの場合フィールドである必要がある。メソッドのスコープ中にRunnableが実行されるとは限らないためだ。
2. Activity#runOnUiThreadメソッドを使用する
public test_click_button() { activity.runOnUiThread(new Runnable(){ @Override public void run() { Button button = (Button)activity.findViewById(R.id.Button01); button.performClick(); } }); }
元々、Activityは内部にHandlerを所有しており、それを使うことで1.と同様の処理を実現できる。
こちらは内部でRunnableの実行の同期が必要か否かを判定しており(同期不要であればHandlerを介さず実行される)、従ってActivityを使用することが判っているのであれば、こちらを使うのがお勧めだ。