Android单元测试:ActivityMonitor的waitForActivityWithTimeout返回NULL,getActivity永远不会返回,INJECT_EVENTS权限错误

4
我正在使用谷歌推荐的Android测试框架:ActivityInstrumentationTestCase2。在随机测试运行过程中我遇到了以下错误,并且持续性致命。这意味着有时所有测试都通过了(开心!),但很多时候它会随机失败,或出现以下三个错误之一。这非常令人沮丧,让我对测试结果没有信心。
为了详细描述这些问题,我提供了我的测试类简化伪代码和以下三个问题。这两个测试用例是相互独立的。
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
    }
}

现在,我一遍又一遍地运行了这两个测试用例(它们将按照字母顺序运行),以下是结果:
  1. 两个测试用例都通过了,或者
  2. testA_HappyPath()失败,因为ActivityMonitor.waitForActivityWithTimeout()返回NULL SecondActivity。但是当我查看我的设备时,SecondActivity正确显示。不知何故,测试未能注意到它。为什么?
  3. 当testA_HappyPath()失败时,然后下一个testB_SadPath()会在setUp() > getActivity()无限期地挂起。我认为我在tearDown()中关闭了所有东西。为什么?
  4. testB_SadPath()经常在TouchUtils.clickView()上失败,并出现以下错误:“注入到另一个应用程序需要INJECT_EVENTS权限”(无论testA_HappyPath是否通过或失败)。为什么?
任何有用的反馈都将不胜感激。谢谢!
到目前为止,我已经处理了这些问题几天时间,研究了互联网上的许多建议,并进行了尝试和错误。没有一个可以立即解决特定问题,但是 - 通过结合我找到的内容,我解决了上述问题(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失败了。 tearDown()被执行得很好。问题在于SecondActivity确实出现在屏幕上,但它从未在finally块中关闭,因为它的方法实例“secondActivity”仍为空。保持SecondActivity处于活动状态并停留在屏幕上导致下一个getActivity()挂起。我通过更改最终块来确保如果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() 中失败,出现以下错误:"Injecting to another application requires INJECT_EVENTS permission"。

我仍然无法解决这个最后的问题 :-(


当我遇到关于INJECT_EVENTS的错误时,是因为我试图按下一个隐藏在软键盘下面的按钮。我正在使用Android Espresso库进行测试,他们有一个实用方法来关闭软键盘,这解决了问题。 - Matt Wolfe
1个回答

1
ISSUE (3) testB_SadPath()经常在使用TouchUtils.clickView()时失败,错误信息如下:"Injecting to another application requires INJECT_EVENTS permission"。
我找到了一种避免此问题的替代方法。在我的Android单元测试中,我不使用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()也执行了同样的操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接