单元测试Android片段

Tre*_*bie 39 android unit-testing fragment android-fragments activityunittestcase

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

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

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

我在其他地方找到的只是扩展AndroidTestCase类的例子,但是所有经过测试的都是将两个数字加在一起,或者如果使用了Context,它只是做一个简单的get并测试某些东西不是null!

据我所知,片段必须存在于一个活动中.那么我可以创建一个模拟Activity,或者获取Application或Context来提供一个Activity,我可以在其中测试我的片段吗?

我是否需要创建自己的Activity然后使用ActivityUnitTestCase?

谢谢你的协助.

崔佛

Kon*_*nov 34

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.

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

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.

1. JUnit testing of fragments

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

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'
    }
    .....
}
Run Code Online (Sandbox Code Playgroud)

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

And class Cow:

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

    public Cow(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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! 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.

2. Instrumental testing of fragments

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

In Build Variants specify Android Instrumental Tests as a Test Artifact: 在此输入图像描述

在Gradle中我添加了这些依赖项:

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"
    }
    .....
}
Run Code Online (Sandbox Code Playgroud)

(再次,几乎所有这些都是可选的,但它们可以让你的生活变得如此简单)

- 如果你没有Dagger

这是一条快乐的道路.与上述Robolectric的不同之处仅在于细节.

前置步骤1:如果您要使用Mockito,则必须启用它才能在设备和模拟器上运行此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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

MainActivityFragment保持不变,如上所述.所以测试集看起来像:

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();
    }

}
Run Code Online (Sandbox Code Playgroud)

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.

- If you do have Dagger

Things are getting serious here :-)

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

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

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;
    }
    .......
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

There're several important things here.

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

2) setUpActivityAndFragment() method:

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() of activity called;

3) onResume() of activity called;

II. attach and starts fragment to the activity

1) onAttach() of fragment called;

2) onCreateView() of fragment called;

3) onStart() of fragment called;

4) onResume() of fragment called;

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

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

MockInjectionRegistration looks like:

....
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;
    }
.........
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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);
    }
    ....
}
Run Code Online (Sandbox Code Playgroud)

而已! 现在,您已为Fragments启用了Instrumental/JUnit测试.

我真诚地希望这篇文章可以帮到某些人.

  • 更简单的方法是将“calculateYoungCows()”方法提取到一个单独的类中,并对其进行简单的单元测试。 (3认同)

abh*_*kar 17

假设您有一个名为"MyFragmentActivity"的FragmentActivity类,其中使用FragmentTransaction添加名为"MyFragment"的公共Fragment类.只需创建一个'JUnit测试用例'类,在您的测试项目中扩展ActivityInstrumentationTestCase2.然后只需调用getActivity()并访问MyFragment对象及其公共成员以编写测试用例.

请参阅下面的代码段:

// TARGET CLASS
public class MyFragmentActivity extends FragmentActivity {
    public MyFragment myFragment;

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

        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        myFragment = new MyFragment();
        fragmentTransaction.add(R.id.mainFragmentContainer, myFragment);
        fragmentTransaction.commit();
    }
}

// TEST CLASS
public class MyFragmentActivityTest extends android.test.ActivityInstrumentationTestCase2<MyFragmentActivity> {
    MyFragmentActivity myFragmentActivity;
    MyFragment myFragment;

    public MyFragmentActivityTest() {
        super(MyFragmentActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        myFragmentActivity = (MyFragmentActivity) getActivity();
        myFragment = myFragmentActivity.myFragment;
    }

    public void testPreConditions() {
        assertNotNull(myFragmentActivity);
        assertNotNull(myFragment);
    }

    public void testAnythingFromMyFragment() {
        // access any public members of myFragment to test
    }
}
Run Code Online (Sandbox Code Playgroud)

我希望这有帮助.如果你觉得这很有用,请接受我的回答.谢谢.

  • 以上示例不是单元测试,而是仪器测试. (3认同)
  • 你如何解决TestRunner(16162):java.lang.RuntimeException:无法解析以下活动:Intent {act = android.intent.action.MAIN flg = 0x10000000 cmp = com.example/.test.MyFragmentActivityTest $ MyFragmentActivity} (2认同)

Dal*_*osa 0

我很确定您可以按照您所说的操作,创建一个模拟活动并从那里测试片段。您只需在主项目中导出兼容性库,然后就可以访问测试项目中的片段。我将创建一个示例项目并在此处测试代码,并将根据我发现的内容更新我的答案。

有关如何导出兼容性库的更多详细信息,请查看此处