当自定义ImageView调用startAnimation(Animation)时,为什么getActivity()在JUnit测试期间会阻塞?

时间:2013-12-31 17:10:56

标签: android junit imageview android-imageview android-testing

我写了一个Android应用,显示使用ImageView定期轮换的自定义startAnimation(Animation)。该应用程序工作正常,但如果我创建类型为ActivityInstrumentationTestCase2的JUnit测试并且测试调用getActivity(),那么对getActivity()的调用永远不会返回,直到应用程序进入后台(例如,按下设备的主页按钮。

经过多长时间和挫折后,如果我在自定义getActivity()课程中注释掉startAnimation(Animation)的来电,我发现ImageView会立即返回。但这会破坏我的自定义ImageView的目的,因为我确实需要为其设置动画。

在JUnit测试期间,有人可以告诉我getActivity()为什么会阻塞,但只有在使用startAnimation时才会这样做?提前感谢任何可以建议解决方法或告诉我我做错了什么的人。

注意:解决方案需要至少使用Android API级别10。

以下是运行它所需的所有源代码(将任何PNG图像放在res / drawable中并将其命名为the_image.png):

activity_main.xml中:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <com.example.rotatingimageviewapp.RotatingImageView 
        android:id="@+id/rotatingImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@drawable/the_image" />

</RelativeLayout>

MainActivity.java:

package com.example.rotatingimageviewapp;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    private RotatingImageView rotatingImageView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        rotatingImageView = (RotatingImageView) findViewById(
                R.id.rotatingImageView);
        rotatingImageView.startRotation();
    }

    @Override
    protected void onPause() {
        super.onPause();
        rotatingImageView.stopRotation();
    }

    @Override
    protected void onResume() {
        super.onResume();
        rotatingImageView.startRotation();
    }

}

RotatingImageView.java(自定义ImageView):

package com.example.rotatingimageviewapp;

import java.util.Timer;
import java.util.TimerTask;

import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;

public class RotatingImageView extends ImageView {

    private static final long ANIMATION_PERIOD_MS = 1000 / 24;

    //The Handler that does the rotation animation
    private final Handler handler = new Handler() {

        private float currentAngle = 0f;
        private final Object animLock = new Object();
        private RotateAnimation anim = null;

        @Override
        public void handleMessage(Message msg) {
            float nextAngle = 360 - msg.getData().getFloat("rotation");
            synchronized (animLock) {
                anim = new RotateAnimation(
                        currentAngle,
                        nextAngle,
                        Animation.RELATIVE_TO_SELF,
                        .5f,
                        Animation.RELATIVE_TO_SELF,
                        .5f);
                anim.setDuration(ANIMATION_PERIOD_MS);
                /**
                 * Commenting out the following line allows getActivity() to
                 * return immediately!
                 */
                startAnimation(anim);
            }

            currentAngle = nextAngle;
        }

    };

    private float rotation = 0f;
    private final Timer timer = new Timer(true);
    private TimerTask timerTask = null;

    public RotatingImageView(Context context) {
        super(context);
    }

    public RotatingImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public RotatingImageView(Context context, AttributeSet attrs,
            int defStyle) {
        super(context, attrs, defStyle);
    }

    public void startRotation() {
        stopRotation();

        /**
         * Set up the task that calculates the rotation value
         * and tells the Handler to do the rotation
         */
        timerTask = new TimerTask() {

            @Override
            public void run() {
                //Calculate next rotation value
                rotation += 15f;
                while (rotation >= 360f) {
                    rotation -= 360f; 
                }

                //Tell the Handler to do the rotation
                Bundle bundle = new Bundle();
                bundle.putFloat("rotation", rotation);
                Message msg = new Message();
                msg.setData(bundle);
                handler.sendMessage(msg);
            }

        };
        timer.schedule(timerTask, 0, ANIMATION_PERIOD_MS);
    }

    public void stopRotation() {
        if (null != timerTask) {
            timerTask.cancel();
        }
    }

}

MainActivityTest.java:

package com.example.rotatingimageviewapp.test;

import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;

import com.example.rotatingimageviewapp.MainActivity;

public class MainActivityTest extends
        ActivityInstrumentationTestCase2<MainActivity> {

    public MainActivityTest() {
        super(MainActivity.class);
    }

    protected void setUp() throws Exception {
        super.setUp();
    }

    protected void tearDown() throws Exception {
        super.tearDown();
    }

    public void test001() {
        assertEquals(1 + 2, 3 + 0);
    }

    public void test002() {
        //Test hangs on the following line until app goes to background
        Activity activity = getActivity();
        assertNotNull(activity);
    }

    public void test003() {
        assertEquals(1 + 2, 3 + 0);
    }

}

5 个答案:

答案 0 :(得分:9)

不确定你们是否解决了这个问题。 但这是我的解决方案,只需覆盖方法getActivity():

@Override
    public MyActivity getActivity() {
        if (mActivity == null) {
            Intent intent = new Intent(getInstrumentation().getTargetContext(), MyActivity.class);
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            // register activity that need to be monitored.
            monitor = getInstrumentation().addMonitor(MyActivity.class.getName(), null, false);
            getInstrumentation().getTargetContext().startActivity(intent);
            mActivity = (MyActivity) getInstrumentation().waitForMonitor(monitor);
            setActivity(mActivity);
        }
        return mActivity;
    }

答案 1 :(得分:1)

我可以告诉你为什么会这样,并且有一个小的解决方法,我认为你应该能够对你的观点做些什么,但这应该适用于现在。

问题是,当你调用getActivity()时,它会经历一系列方法,直到它在InstrumentationTestCase.java中遇到以下内容

public final <T extends Activity> T launchActivityWithIntent(
            String pkg,
            Class<T> activityCls,
            Intent intent) {
        intent.setClassName(pkg, activityCls.getName());
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        T activity = (T) getInstrumentation().startActivitySync(intent);
        getInstrumentation().waitForIdleSync();
        return activity;
    }

问题是令人讨厌的线路有以下几点:

getInstrumentation().waitForIdleSync();

由于你的动画,主线程上永远不会有空闲,所以它永远不会从这个方法返回!你怎么解决这个问题?好吧,它很容易你必须覆盖这个方法,所以它不再有那条线。你可能需要添加一些代码来等待确保活动被启动,否则这个方法将返回太快!我建议等待特定于此活动的观点。

答案 2 :(得分:0)

更新:感谢@nebula的上述答案:https://stackoverflow.com/a/24506584/720773


我了解了这个问题的一个简单的解决方法:使用不同的方法来旋转图像,这不涉及Animation

Android: Rotate image in imageview by an angle

这并没有真正回答我的问题,但它解决了这个问题。如果有人知道如何在自定义ActivityInstrumentationTestCase2.getActivity()中使用Activity课时让Animation返回ImageView,请发布SSCCE作为答案,我如果有效,我会接受它,而不是这个。

答案 3 :(得分:0)

我了解了这个问题的每个解决方法,这是我的解决方案,它适用于每个人;)

public class TestApk extends ActivityInstrumentationTestCase2 {

    private static final String LAUNCHER_ACTIVITY_FULL_CLASSNAME =
        "com.notepad.MainActivity";
    private static Class launcherActivityClass;
    static {

        try {
            launcherActivityClass = Class
                    .forName(LAUNCHER_ACTIVITY_FULL_CLASSNAME);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public TestApk () throws ClassNotFoundException {
        super(launcherActivityClass);
    }

    private Solo solo;

    @Override
    protected void setUp() throws Exception {
        solo = new Solo(getInstrumentation());
        Intent intent = new Intent(getInstrumentation().getTargetContext(), launcherActivityClass);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getInstrumentation().getTargetContext().startActivity(intent);
    }

    public void test_ookla_speedtest() {
        Boolean expect = solo.waitForText("Login", 0, 60*1000);
        assertTrue("xxxxxxxxxxxxxxxxxxx", expect);
    }

    @Override
    public void tearDown() throws Exception {
        solo.finishOpenedActivities();
        super.tearDown();
    }

}

答案 4 :(得分:0)

我相信保罗·哈里斯(Paul Harris)正确回答了发生此问题的原因。那么,如何才能更轻松地解决此问题呢?答案很简单,如果处于测试模式,请不要启动动画。那么如何判断您是否处于测试模式?有几种方法可以做到这一点,但是一种简单的方法就是向您用来在测试中开始活动的意图添加一些额外的数据。我将以使用AndroidJUnit的方式给出示例代码(我的理解是不推荐使用ActivityInstrumentationTestCase2,或者至少是AndroidJUnit是进行有条件的测试的新方法;而且我还假设AndroidJUnit也会对waitForIdleSync进行调用,尚未验证)

@Rule
public ActivityTestRule<MainActivity> mActivityRule =
        new ActivityTestRule<>(MainActivity.class, true, false);

@Before
    public init() {
    Activity mActivity;
    Intent intent = new Intent();
    intent.put("isTestMode, true);
    mActivity = mActivityRule.launchActivity(intent);
}

因此,在您的MainActivity onCreate方法中,执行以下操作:

Boolean isTestMode = (Boolean)savedInstanceState.get("isTestMode");
if (isTestMode == null || !isTestMode) {
    rotatingImageView.startRotation();
}

启动活动后,如果对您很重要,则可以使用其他方法来启动Rotation。