4

Google が推奨する Android テスト フレームワークを使用しています: ActivityInstrumentationTestCase2. ランダムなテストの実行で次のエラーが発生しましたが、永続的な致命的なエラーが発生しました。これは、すべてのテストが成功することもあれば (幸いなことに!)、多くの場合、これら 3 つのエラーのいずれかでランダムに失敗することを意味します。もどかしく、テスト結果に自信が持てません。

問題を詳しく説明するために、テスト クラスの簡略化された疑似コードと以下の 3 つの問題を提供しました。両方のテスト ケースは互いに独立しています。

public class FirstActivityTest 
extends ActivityInstrumentationTestCase2<FirstActivity> {
    private FirstActivity mActivity;
    private ActivityMonitor mActivityMonitor;

    public FirstActivityTest () {
        super(FirstActivity.class);
    }

    public void setUp() throws Exception {
        super.setUp();
        setActivityInitialTouchMode(false);

        mActivity = getActivity();
        assertNotNull("Cannot start test since target Activity is NULL!", mActivity);

        mActivityMonitor = getInstrumentation().addMonitor(SecondActivity.class.getName(), null, false);
    }

    public void tearDown() throws Exception {
        super.tearDown();
        if(mActivity != null) {
            mActivity.finish();
            mActivity = null;
        }
        if(mActivityMonitor != null) {
            getInstrumentation().removeMonitor(mActivityMonitor);
            mActivityMonitor = null;
        }
    }

    /**
     * Open FirstActivity, enter a text and click submit button. 
     * Verifies SecondActivity is open.
     */
    public void testA_HappyPath() {
        Activity secondActivity = null;
        try {
            //(Omitted) Get edit text and enter a valid value
            //(Omitted) Find submitButton view
            //Click submit button
            TouchUtils.clickView(this, submitButton);

            //Wait for result and validate:
            secondActivity = mActivityMonitor.waitForActivityWithTimeout(10000);
            assertNotNull("Result SecondActivity should NOT be null!", secondActivity );
        } finally {
            //Clean up:
            if(secondActivity != null) {
                secondActivity .finish();
                secondActivity = null;
            }
        }
    }

    /**
     * Open FirstActivity, do NOT enter a text and click submit button. 
     * Verifies error message is returned.
     */
    public void testB_SadPath() {
        //(Omitted) Find submitButton view
        //Click submit button
        TouchUtils.clickView(this, submitButton);

        //(Omitted) Validate error message is displayed
    }
}

ここで、これら 2 つのテスト ケースを何度も実行しました (アルファベット順に実行します)。結果は次のとおりです。

  1. 両方のテスト ケースに合格、または
  2. ActivityMonitor.waitForActivityWithTimeout() が NULL SecondActivity を返したため、testA_HappyPath() は失敗しました。しかし、自分のデバイスを見ると、SecondActivity が正しく表示されています。どういうわけか、テストはそれに気づきませんでした。なんで?
  3. testA_HappyPath() が失敗すると、次の testB_SadPath() が setUp() > getActivity() の間に無期限にハングしていました。私は、tearDown() 内のすべてをシャットダウンしたと思います。なんで?
  4. testB_SadPath () は TouchUtils.clickView() で次のエラーで失敗することがよくありました。なんで?

役立つフィードバックをお待ちしております。ありがとう!


私はこれら 3 つの問題に数日間対処し、インターネット上で多くの提案を研究し、試行錯誤を繰り返しました。ただし、特定の問題をすぐに解決できるものは1つもありませんでした-見つけたものを組み合わせることで、上記の問題(1)と(2)を解決しましたが、まだ問題(3)が解決されていません。以下は、この作業を行うために私が行った修正の詳細です。

問題 (1) ActivityMonitor.waitForActivityWithTimeout() が NULL を返す

1.1。getActivit() の前に getInstrumentation().addMonitor() を宣言する必要があることを学びました。setUp() メソッドをどのように変更したかを確認してください。これにより、問題が何らかの形で修正されました。これが要件である理由を理解している方は、お知らせください。

1.2. エミュレーターでは、この呼び出しが NULL を返すことがあり、テストが失敗することがありました。待ち時間が短すぎたことが原因だとわかりました。したがって、待機時間を長くすると、ActivityMonitor の戻りが早すぎるのを防ぐことができます。

問題 (2) 次の testB_SadPath() は、setUp() > getActivity() の間に無期限にハングします。

2.1. 上で説明したように、これは前のテスト (testA_HappyPath) が失敗したときに発生しました。私は、tearDown() がすべてをクリーンアップし、次のテストを実行する準備ができていると考えました。何が起こったのかというと、testA は SecondActivity が画面に表示されるのを待っていましたが、ActivityMonitor.waitForActivityWithTimeout() が NULL を返したため、testA は失敗しました。teaDown() は問題なく実行されました。問題は、SecondActivity が実際に画面に表示されたが、そのメソッド インスタンス 'secondActivity' がまだ null だったため、finally ブロックでシャットダウンされないことです。SecondActivity が有効で画面に残っていると、次の getActivity() がハングしていました。これを修正するには、finally ブロックを変更して、SecondActivity が存在するかどうかを確認し、シャットダウンするようにしました。

これらの変更は、以下のコードに要約されています (setUp() および finally ブロックを参照してください)。

public class FirstActivityTest 
extends ActivityInstrumentationTestCase2<FirstActivity> {
    private FirstActivity mActivity;
    private ActivityMonitor mActivityMonitor;

    public FirstActivityTest () {
        super(FirstActivity.class);
    }

    public void setUp() throws Exception {
        super.setUp();
        setActivityInitialTouchMode(false);
    }

    public void tearDown() throws Exception {
        super.tearDown();
        if(mActivity != null) {
            mActivity.finish();
            mActivity = null;
        }
        if(mActivityMonitor != null) {
            getInstrumentation().removeMonitor(mActivityMonitor);
            mActivityMonitor = null;
        }
    }

    /**
     * Open FirstActivity, enter a text and click submit button. 
     * Verifies SecondActivity is open.
     */
    public void testA_HappyPath() {
        mActivityMonitor = getInstrumentation().addMonitor(SecondActivity.class.getName(), null, false);

        mActivity = getActivity();
        assertNotNull("Cannot start test since target Activity is NULL!", mActivity);

        Activity secondActivity = null;
        try {
            //(Omitted) Get edit text and enter a valid value
            //(Omitted) Find submitButton view
            //Click submit button
            TouchUtils.clickView(this, submitButton);

            //Wait for result and validate:
            secondActivity = mActivityMonitor.waitForActivityWithTimeout(20000);
            assertNotNull("Result SecondActivity should NOT be null!", secondActivity );
        } finally {
            //Clean up:
            if(secondActivity == null) {
                //If empty, wait longer because need to shut down the foreground activity, if any: 
                secondActivity = mActivityMonitor.waitForActivityWithTimeout(20000);
            }
            if(secondActivity != null) {
                secondActivity .finish();
                secondActivity = null;
            }
        }
    }

    /**
     * Open FirstActivity, do NOT enter a text and click submit button. 
     * Verifies error message is returned.
     */
    public void testB_SadPath() {
        mActivity = getActivity();
        assertNotNull("Cannot start test since target Activity is NULL!", mActivity);

        //(Omitted) Find submitButton view
        //Click submit button
        TouchUtils.clickView(this, submitButton);

        //(Omitted) Validate error message is displayed
    }
}

問題 (3) testB_SadPath() は、次のエラーで TouchUtils.clickView() で失敗することがよくありました: 「別のアプリケーションへの注入には INJECT_EVENTS 権限が必要です」

この最後の問題はまだ解決できません:-(

4

1 に答える 1

1

問題 (3) testB_SadPath() は、次のエラーで TouchUtils.clickView() で失敗することがよくありました: 「別のアプリケーションへの注入には INJECT_EVENTS 権限が必要です」

Android Unit Test でこの問題を回避する別の方法を見つけました。TouchUtils.clickView() を使用する代わりに、performClick() を呼び出して、ボタン自体で直接クリック操作を実行します。次の変更されたテスト コードは、時々発生する INJECT_EVENTS パーミッション エラーを解決します。特に、populateDataAndClickSubmit() を参照してください。

/**
 * Open FirstActivity, enter a text and click submit button. 
 * Verifies SecondActivity is open.
 */
public void testA_HappyPath() {
    mActivityMonitor = getInstrumentation().addMonitor(SecondActivity.class.getName(), null, false);

    mActivity = getActivity();
    assertNotNull("Cannot start test since target Activity is NULL!", mActivity);

    Activity secondActivity = null;
    try {
        String dataValue = "MyNameIsNoLongerFooNorBar";
        populateDataAndClickSubmit(dataValue);

        //Wait for result and validate:
        secondActivity = mActivityMonitor.waitForActivityWithTimeout(20000);
        assertNotNull("Result SecondActivity should NOT be null!", secondActivity );
    } finally {
        //Clean up:
        if(secondActivity == null) {
            //If empty, wait longer because need to shut down the foreground activity, if any: 
            secondActivity = mActivityMonitor.waitForActivityWithTimeout(20000);
        }
        if(secondActivity != null) {
            secondActivity.finish();
            secondActivity = null;
        }
    }
}

/**
 * Open FirstActivity, do NOT enter a text and click submit button. 
 * Verifies error message is returned.
 */
public void testB_SadPath() {
    mActivity = getActivity();
    assertNotNull("Cannot start test since target Activity is NULL!", mActivity);

    String dataValue = null;
    populateDataAndClickSubmit(dataValue);

    //(Omitted) Validate error message is displayed
}

private void populateDataAndClickSubmit(final String dataValueString) {
    final EditText editDataView = //(omitted) find it from the activity layout
    final Button submitButton = //(Omitted) Find submitButton view

    mActivity.runOnUiThread(
            new Runnable() {
                public void run() {
                    editDataView.setText(dataValueString);
                    submitButton.performClick();
               }
            }
        );

    //Wait and allow app to be idle while performClick to finish and activity re-drawn:
    getInstrumentation().waitForIdleSync();
}

ノート:

  1. この解決策は、 touchUtils.clickView() が時折インジェクト イベント許可エラーをスローする理由に対する答えではありません。
  2. View.performClick() では、問題のビューに OnClickListener() を実装するアクティビティが必要です。私の場合、SubmitButton は既に持っているので、便利なテスト コードの変更です。
  3. getInstrumentation().waitForIdleSync() を使用すると、アプリが作業を完了してレイアウトを適切に再描画するまで、テスト コードをアイドル状態にすることができます。Java コードを見ると、これと同じ行が touchUtils.clickView() 内で実行されます。
于 2013-09-27T19:20:23.077 に答える