如何在Android中将单元测试设置为片段 [英] How to set Unit Test to Fragment in Android

查看:143
本文介绍了如何在Android中将单元测试设置为片段的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想对Android Fragment类进行单元测试.

I want to unit test an Android Fragment class.

我可以使用AndroidTestCase设置测试还是需要使用ApplicationTestCase?

Can I set up a test using AndroidTestCase or do I need to use ApplicationTestCase?

有没有关于如何使用这两个TestCases的有用示例?开发人员站点上的测试示例很少,似乎只专注于测试活动.

Are there any useful examples of how these two TestCases can be used? The testing examples on the developer site are minimal and just seem to focus on testing Activities.

我在其他地方找到的所有示例都是扩展了AndroidTestCase类的示例,但随后所有经过测试的就是将两个数字加在一起,或者如果使用了Context,它只会执行一次简单的get并测试不为null的东西!

All I've found elsewhere are examples where the AndroidTestCase class is extended but then all that's tested is adding two numbers together or if the Context is used, it just does a simple get and tests that something is not null!

据我了解,片段必须存在于活动中.那么我可以创建一个模拟活动,还是获取应用程序或上下文来提供一个可以测试我的片段的活动?

As I understand it, a Fragment has to live within an Activity. So could I create a mock Activity, or get the Application or Context to provide an Activity within which I can test my Fragment?

我需要创建自己的Activity,然后使用ActivityUnitTestCase吗?

Do I need to create my own Activity and then use ActivityUnitTestCase?

推荐答案

我一直在努力解决相同的问题.特别是,由于大多数代码示例已经过时+ Android Studio/SDK正在改进,因此旧答案有时不再适用.

I was struggling with same question. Especially, as most of code samples are already outdated + Android Studio/SDKs is improving, so old answers sometimes are not relevant anymore.

因此,首先要做的是:您需要确定要使用 Instrumental 还是简单的 JUnit 测试.

So, first things first: you need to determine if you want to use Instrumental or simple JUnit tests.

它们之间的区别由S.D. 此处; 简而言之:JUnit测试更轻巧,不需要运行仿真器,即仪器-为您提供最接近实际设备的可能体验(传感器,gps,与其他应用程序的交互等).另请阅读有关在Android中进行测试的更多信息.

The difference between them beautifully described by S.D. here; In short: JUnit tests are more lightweight and not require an emulator to run, Instrumental - give you the closest to the actual device possible experience (sensors, gps, interaction with other apps etc.). Also read more about testing in Android.

比方说,您不需要繁重的工具测试,简单的junit测试就足够了. 为此,我使用了很好的框架 Robolectric .

Let's say, you don't need heavy Instrumental tests and simple junit tests are enough. I use nice framework Robolectric for this purpose.

在gradle中添加:

In gradle add:

dependencies {
    .....
    testCompile 'junit:junit:4.12'
    testCompile 'org.robolectric:robolectric:3.0'
    testCompile "org.mockito:mockito-core:1.10.8"
    testCompile ('com.squareup.assertj:assertj-android:1.0.0') {
        exclude module: 'support-annotations'
    }
    .....
}

Mockito,AsserJ是可选的,但我发现它们非常有用,因此我强烈建议也将它们包括在内.

Mockito, AsserJ are optional, but I found them very useful so I highly recommend to include them too.

然后在 Build Variants 中将单元测试指定为 Test Artifact :

Then in Build Variants specify Unit Tests as a Test Artifact:

现在是时候编写一些真实的测试了:-) 例如,以标准的带片段的空白活动"示例项目为例.

Now it's time to write some real tests :-) As an example, lets take the standard "Blank Activity with Fragment" sample project.

我添加了一些代码行,实际上要进行一些测试:

I added some lines of code, to have actually something to test:

import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;

public class MainActivityFragment extends Fragment {

    private List<Cow> cows;
    public MainActivityFragment() {}

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {   
        cows = new ArrayList<>();
        cows.add(new Cow("Burka", 10));
        cows.add(new Cow("Zorka", 9));
        cows.add(new Cow("Kruzenshtern", 15));

        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    int calculateYoungCows(int maxAge) {
        if (cows == null) {
            throw new IllegalStateException("onCreateView hasn't been called");
        }

        if (getActivity() == null) {
            throw new IllegalStateException("Activity is null");
        }

        if (getView() == null) {
            throw new IllegalStateException("View is null");
        }

        int result = 0;
        for (Cow cow : cows) {
            if (cow.age <= maxAge) {
                result++;
            }
        }

        return result;
    }
}

然后上课Cow:

public class Cow {
    public String name;
    public int age;

    public Cow(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

Robolectic的测试集如下所示:

The Robolectic's test set would look something like:

import android.app.Application;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.test.ApplicationTestCase;

import junit.framework.Assert;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk=21)
public class MainActivityFragmentTest extends ApplicationTestCase<Application> {

    public MainActivityFragmentTest() {
        super(Application.class);
    }

    MainActivity mainActivity;
    MainActivityFragment mainActivityFragment;

    @Before
    public void setUp() {
        mainActivity = Robolectric.setupActivity(MainActivity.class);
        mainActivityFragment = new MainActivityFragment();
        startFragment(mainActivityFragment);
    }

    @Test
    public void testMainActivity() {
        Assert.assertNotNull(mainActivity);
    }

    @Test
    public void testCowsCounter() {
        assertThat(mainActivityFragment.calculateYoungCows(10)).isEqualTo(2);
        assertThat(mainActivityFragment.calculateYoungCows(99)).isEqualTo(3);
    }

    private void startFragment( Fragment fragment ) {
        FragmentManager fragmentManager = mainActivity.getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(fragment, null );
        fragmentTransaction.commit();
    }
}

即我们通过 Robolectric.setupActivity (测试类的setUp()中的新片段)创建活动. (可选)您可以立即从setUp()启动片段,也可以直接从测试中执行片段.

I.e. we create activity via Robolectric.setupActivity, new fragment in the test-classes' setUp(). Optionally, you can immediately start the fragment from the setUp() or you can do it directly from the test.

NB!我还没花太多时间,但似乎几乎不可能将它与Dagger绑在一起(我不知道是否使用Dagger2会更容易,因为您无法使用模拟的注入设置自定义测试应用程序.

NB! I haven't spent too much time on it, but it looks like it's almost impossible to tie it together with Dagger(I don't know if it's easier with Dagger2), as you can't set custom test Application with mocked injections.

这种方法的复杂性高度取决于您是否要在要测试的应用程序中使用Dagger/Dependency注入.

The complexity of this approach is highly depends on if you're using Dagger/Dependency injection in the app you want to test.

Build Variants 中将 Android Instrumental Tests 指定为 Test Artifact :

在Gradle中,我添加了以下依赖项:

In Gradle I add these dependencies:

dependencies {
    .....
    androidTestCompile "com.google.dexmaker:dexmaker:1.1"
    androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.1"
    androidTestCompile 'com.squareup.assertj:assertj-android:1.0.0'
    androidTestCompile "org.mockito:mockito-core:1.10.8"
    }
    .....
}

(同样,它们几乎都是可选的,但它们可以使您的生活更加轻松)

(again, pretty much all of them are optional, but they can make your life so much easier)

这是一条快乐的道路.与Robolectric的区别仅在于很小的细节.

This a happy path. The difference with Robolectric from the above would be only in small details.

第1步:如果您要使用Mockito,则必须通过此hack使其在设备和仿真器上运行:

Pre-step 1: If you are going to use Mockito, you have to enable it to run on the devices and emulators with this hack:

public class TestUtils {
    private static final String CACHE_DIRECTORY = "/data/data/" + BuildConfig.APPLICATION_ID + "/cache";
    public static final String DEXMAKER_CACHE_PROPERTY = "dexmaker.dexcache";

    public static void enableMockitoOnDevicesAndEmulators() {
        if (System.getProperty(DEXMAKER_CACHE_PROPERTY) == null || System.getProperty(DEXMAKER_CACHE_PROPERTY).isEmpty()) {
            File file = new File(CACHE_DIRECTORY);
            if (!file.exists()) {
                final boolean success = file.mkdirs();
                if (!success) {
                    fail("Unable to create cache directory required for Mockito");
                }
            }

            System.setProperty(DEXMAKER_CACHE_PROPERTY, file.getPath());
        }
    }
}

MainActivityFragment保持不变,如上所述.因此测试集如下所示:

The MainActivityFragment stays the same, as above. So the test-set would look like:

package com.klogi.myapplication;

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.test.ActivityInstrumentationTestCase2;

import junit.framework.Assert;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class MainActivityFragmentTest extends ActivityInstrumentationTestCase2<MainActivity> {

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

    MainActivity mainActivity;
    MainActivityFragment mainActivityFragment;

    @Override
    protected void setUp() throws Exception {
        TestUtils.enableMockitoOnDevicesAndEmulators();
        mainActivity = getActivity();
        mainActivityFragment = new MainActivityFragment();
    }

    public void testMainActivity() {
        Assert.assertNotNull(mainActivity);
    }

    public void testCowsCounter() {
        startFragment(mainActivityFragment);
        assertThat(mainActivityFragment.calculateYoungCows(10)).isEqualTo(2);
        assertThat(mainActivityFragment.calculateYoungCows(99)).isEqualTo(3);
    }

    private void startFragment( Fragment fragment ) {
        FragmentManager fragmentManager = mainActivity.getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(fragment, null);
        fragmentTransaction.commit();

        getActivity().runOnUiThread(new Runnable() {
            @Override
            public void run() {
                getActivity().getSupportFragmentManager().executePendingTransactions();
            }
        });

        getInstrumentation().waitForIdleSync();
    }

}

如您所见,Test类是 ActivityInstrumentationTestCase2 类的扩展. 另外,注意 startFragment 方法也非常重要,该方法与JUnit示例相比已经发生了变化:默认情况下,测试不在UI线程上运行,我们需要显式调用执行以等待FragmentManager的事务.

As you can see, Test class is an extension of ActivityInstrumentationTestCase2 class. Also, it's very important to pay attention to startFragment method, that has changed comparing to JUnit example: by default, tests are not running on the UI thread and we need to explicitly call for execution pending FragmentManager's transactions.

这里的情况越来越严重:-)

Things are getting serious here :-)

首先,我们放弃了 ActivityInstrumentationTestCase2 ,而将 ActivityUnitTestCase 类作为所有片段测试类的基类.

First, we are getting rid of ActivityInstrumentationTestCase2 in favor of ActivityUnitTestCase class, as a base class for all fragment's test classes.

像往常一样,它不是那么简单,并且存在多个陷阱(是示例之一).因此,我们需要将 AcitivityUnitTestCase 插入到 ActivityUnitTestCaseOverride

As usual, it's not that simple and there're several pitfalls (this is one of examples). So we need to pimp our AcitivityUnitTestCase to ActivityUnitTestCaseOverride

将其完整地发布到这里有点太长,所以我将其完整版本上传到 github ;

It's a bit too long to post it fully here, so I upload full version of it to github;

public abstract class ActivityUnitTestCaseOverride<T extends Activity>
        extends ActivityUnitTestCase<T> {

    ........
    private Class<T> mActivityClass;

    private Context mActivityContext;
    private Application mApplication;
    private MockParent mMockParent;

    private boolean mAttached = false;
    private boolean mCreated = false;

    public ActivityUnitTestCaseOverride(Class<T> activityClass) {
        super(activityClass);
        mActivityClass = activityClass;
    }

    @Override
    public T getActivity() {
        return (T) super.getActivity();
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        // default value for target context, as a default
        mActivityContext = getInstrumentation().getTargetContext();
    }

    /**
     * Start the activity under test, in the same way as if it was started by
     * {@link android.content.Context#startActivity Context.startActivity()}, providing the
     * arguments it supplied.  When you use this method to start the activity, it will automatically
     * be stopped by {@link #tearDown}.
     * <p/>
     * <p>This method will call onCreate(), but if you wish to further exercise Activity life
     * cycle methods, you must call them yourself from your test case.
     * <p/>
     * <p><i>Do not call from your setUp() method.  You must call this method from each of your
     * test methods.</i>
     *
     * @param intent                       The Intent as if supplied to {@link android.content.Context#startActivity}.
     * @param savedInstanceState           The instance state, if you are simulating this part of the life
     *                                     cycle.  Typically null.
     * @param lastNonConfigurationInstance This Object will be available to the
     *                                     Activity if it calls {@link android.app.Activity#getLastNonConfigurationInstance()}.
     *                                     Typically null.
     * @return Returns the Activity that was created
     */
    protected T startActivity(Intent intent, Bundle savedInstanceState,
                              Object lastNonConfigurationInstance) {
        assertFalse("Activity already created", mCreated);

        if (!mAttached) {
            assertNotNull(mActivityClass);
            setActivity(null);
            T newActivity = null;
            try {
                IBinder token = null;
                if (mApplication == null) {
                    setApplication(new MockApplication());
                }
                ComponentName cn = new ComponentName(getInstrumentation().getTargetContext(), mActivityClass.getName());
                intent.setComponent(cn);
                ActivityInfo info = new ActivityInfo();
                CharSequence title = mActivityClass.getName();
                mMockParent = new MockParent();
                String id = null;

                newActivity = (T) getInstrumentation().newActivity(mActivityClass, mActivityContext,
                        token, mApplication, intent, info, title, mMockParent, id,
                        lastNonConfigurationInstance);
            } catch (Exception e) {
                assertNotNull(newActivity);
            }

            assertNotNull(newActivity);
            setActivity(newActivity);

            mAttached = true;
        }

        T result = getActivity();
        if (result != null) {
            getInstrumentation().callActivityOnCreate(getActivity(), savedInstanceState);
            mCreated = true;
        }
        return result;
    }

    protected Class<T> getActivityClass() {
        return mActivityClass;
    }

    @Override
    protected void tearDown() throws Exception {

        setActivity(null);

        // Scrub out members - protects against memory leaks in the case where someone
        // creates a non-static inner class (thus referencing the test case) and gives it to
        // someone else to hold onto
        scrubClass(ActivityInstrumentationTestCase.class);

        super.tearDown();
    }

    /**
     * Set the application for use during the test.  You must call this function before calling
     * {@link #startActivity}.  If your test does not call this method,
     *
     * @param application The Application object that will be injected into the Activity under test.
     */
    public void setApplication(Application application) {
        mApplication = application;
    }
    .......
}

为您的所有片段测试创建一个抽象AbstractFragmentTest:

Create an abstract AbstractFragmentTest for all your fragment tests:

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;

/**
 * Common base class for {@link Fragment} tests.
 */
public abstract class AbstractFragmentTest<TFragment extends Fragment, TActivity extends FragmentActivity> extends ActivityUnitTestCaseOverride<TActivity> {

    private TFragment fragment;
    protected MockInjectionRegistration mocks;

    protected AbstractFragmentTest(TFragment fragment, Class<TActivity> activityType) {
        super(activityType);
        this.fragment = parameterIsNotNull(fragment);
    }

    @Override
    protected void setActivity(Activity testActivity) {
        if (testActivity != null) {
            testActivity.setTheme(R.style.AppCompatTheme);
        }

        super.setActivity(testActivity);
    }

    /**
     * Get the {@link Fragment} under test.
     */
    protected TFragment getFragment() {
        return fragment;
    }

    protected void setUpActivityAndFragment() {
        createMockApplication();

        final Intent intent = new Intent(getInstrumentation().getTargetContext(),
                getActivityClass());
        startActivity(intent, null, null);
        startFragment(getFragment());

        getInstrumentation().callActivityOnStart(getActivity());
        getInstrumentation().callActivityOnResume(getActivity());
    }

    private void createMockApplication() {
        TestUtils.enableMockitoOnDevicesAndEmulators();

        mocks = new MockInjectionRegistration();
        TestApplication testApplication = new TestApplication(getInstrumentation().getTargetContext());
        testApplication.setModules(mocks);
        testApplication.onCreate();
        setApplication(testApplication);
    }

    private void startFragment(Fragment fragment) {
        FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(fragment, null);
        fragmentTransaction.commit();
    }
}

这里有几件重要的事情.

There're several important things here.

1)我们将覆盖 setActivity()方法,以将AppCompact主题设置为活动.否则,测试服将崩溃.

1) We override setActivity() method to set the AppCompact theme to the activity. Without that, test suit will crash.

2) setUpActivityAndFragment()方法:

2) setUpActivityAndFragment() method:

I.创建活动(=> getActivity()开始在测试中以及正在测试的应用中返回非null值) 1)调用了活动的onCreate();

I. creates activity ( => getActivity() starts returning non-null value, in tests and in the app which is under test) 1) onCreate() of activity called;

2)调用了活动的onStart();

2) onStart() of activity called;

3)调用了活动的onResume();

3) onResume() of activity called;

II.附加并开始对该活动进行分段

II. attach and starts fragment to the activity

1)片段的onAttach();

1) onAttach() of fragment called;

2)调用了片段的onCreateView();

2) onCreateView() of fragment called;

3)调用了片段的onStart();

3) onStart() of fragment called;

4)被调用的片段的onResume();

4) onResume() of fragment called;

3):createMockApplication()方法: 与非匕首版本一样,在第一步中,我们在设备和仿真器上启用了模拟.

3) createMockApplication() method: As in the non-dagger version, in Pre-step 1, we enable mocking on the devices and on the emulators.

然后我们用自定义TestApplication将普通应用程序替换为其注入!

Then we replace the normal application with its injections with our custom, TestApplication!

MockInjectionRegistration 如下:

....
import javax.inject.Singleton;

import dagger.Module;
import dagger.Provides;
import de.greenrobot.event.EventBus;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

@Module(
        injects = {

                ....
                MainActivity.class,
                MyWorkFragment.class,
                HomeFragment.class,
                ProfileFragment.class,
                ....
        },
        addsTo = DelveMobileInjectionRegistration.class,
        overrides = true
)
public final class MockInjectionRegistration {

    .....
    public DataSource dataSource;
    public EventBus eventBus;
    public MixpanelAPI mixpanel;
    .....

    public MockInjectionRegistration() {
        .....
        dataSource = mock(DataSource.class);
        eventBus = mock(EventBus.class);
        mixpanel = mock(MixpanelAPI.class);
        MixpanelAPI.People mixpanelPeople = mock(MixpanelAPI.People.class);
        when(mixpanel.getPeople()).thenReturn(mixpanelPeople);
        .....
    }
...........
    @Provides
    @Singleton
    @SuppressWarnings("unused")
        // invoked by Dagger
    DataSource provideDataSource() {
        Guard.valueIsNotNull(dataSource);
        return dataSource;
    }

    @Provides
    @Singleton
    @SuppressWarnings("unused")
        // invoked by Dagger
    EventBus provideEventBus() {
        Guard.valueIsNotNull(eventBus);
        return eventBus;
    }

    @Provides
    @Singleton
    @SuppressWarnings("unused")
        // invoked by Dagger
    MixpanelAPI provideMixpanelAPI() {
        Guard.valueIsNotNull(mixpanel);
        return mixpanel;
    }
.........
}

即我们向片段提供其模拟版本,而不是真实的类. (这很容易跟踪,可以配置方法调用的结果,等等.)

I.e. instead of real classes, we are providing to the fragments their mocked versions. (That are easily traceable, allows to configure results of method calls, etc.).

TestApplication只是您对Application的自定义扩展,应该支持设置模块并初始化ObjectGraph.

And the TestApplication is just your custom extension of Application, that should support setting modules and initialize the ObjectGraph.

这些是开始编写测试的准备步骤:) 现在,简单的部分是真正的测试:

These were pre-steps for start writing the tests :) Now the simple part, the real tests:

public class SearchFragmentTest extends AbstractFragmentTest<SearchFragment, MainActivity> {

    public SearchFragmentTest() {
        super(new SearchFragment(), MainActivity.class);
    }

    @UiThreadTest
    public void testOnCreateView() throws Exception {
        setUpActivityAndFragment();

        SearchFragment searchFragment = getFragment();
        assertNotNull(searchFragment.adapter);
        assertNotNull(SearchFragment.getSearchAdapter());
        assertNotNull(SearchFragment.getSearchSignalLogger());
    }

    @UiThreadTest
    public void testOnPause() throws Exception {
        setUpActivityAndFragment();

        SearchFragment searchFragment = getFragment();
        assertTrue(Strings.isNullOrEmpty(SharedPreferencesTools.getString(getActivity(), SearchFragment.SEARCH_STATE_BUNDLE_ARGUMENT)));

        searchFragment.searchBoxRef.setCurrentConstraint("abs");
        searchFragment.onPause();

        assertEquals(searchFragment.searchBoxRef.getCurrentConstraint(), SharedPreferencesTools.getString(getActivity(), SearchFragment.SEARCH_STATE_BUNDLE_ARGUMENT));
    }

    @UiThreadTest
    public void testOnQueryTextChange() throws Exception {
        setUpActivityAndFragment();
        reset(mocks.eventBus);

        getFragment().onQueryTextChange("Donald");
        Thread.sleep(300);

        // Should be one cached, one uncached event
        verify(mocks.eventBus, times(2)).post(isA(SearchRequest.class));
        verify(mocks.eventBus).post(isA(SearchLoadingIndicatorEvent.class));
    }

    @UiThreadTest
    public void testOnQueryUpdateEventWithDifferentConstraint() throws Exception {
        setUpActivityAndFragment();

        reset(mocks.eventBus);

        getFragment().onEventMainThread(new SearchResponse(new ArrayList<>(), "Donald", false));

        verifyNoMoreInteractions(mocks.eventBus);
    }
    ....
}

就是这样! 现在,您已经为片段启用了Instrumental/JUnit测试.

That's it! Now you have Instrumental/JUnit tests enabled for your Fragments.

我衷心希望这篇文章对某人有所帮助.

I sincerely hope this post helps someone.

这篇关于如何在Android中将单元测试设置为片段的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆