Модульное тестирование фрагмента Android - PullRequest
36 голосов
/ 20 ноября 2011

Я хочу провести модульное тестирование класса Android Fragment.

Можно ли настроить тест с помощью AndroidTestCase или мне нужно использовать ApplicationTestCase?

Есть ли полезные примеры использования этих двух тестовых случаев? Примеры тестирования на сайте разработчика минимальны и, похоже, сосредоточены только на тестировании.

Все, что я нашел в другом месте, это примеры, где класс AndroidTestCase расширен, но затем все, что тестируется, - это сложение двух чисел вместе или, если используется контекст, он просто выполняет простое получение и проверяет, что что-то не является нулевым!

Насколько я понимаю, Фрагмент должен жить в Деятельности. Так можно ли создать фиктивное действие или получить приложение или контекст для предоставления действия, в рамках которого я могу протестировать свой фрагмент?

Нужно ли создавать свою собственную активность, а затем использовать ActivityUnitTestCase?

Спасибо за вашу помощь.

Trev

Ответы [ 4 ]

32 голосов
/ 30 августа 2015

Я боролся с тем же вопросом. Тем более, что большинство примеров кода уже устарели + Android Studio / SDK улучшается, поэтому старые ответы иногда уже не актуальны.

Итак, обо всем по порядку: вам нужно определить, хотите ли вы использовать Инструментальные или простые JUnit тесты.

Разница между ними прекрасно описана С.Д. здесь ; Вкратце: тесты JUnit более легкие и не требуют запуска эмулятора, инструментальные - дают вам максимально возможный опыт работы с устройством (датчики, GPS, взаимодействие с другими приложениями и т. Д.). Также читайте больше о тестировании в Android .

1. JUnit тестирование фрагментов

Допустим, вам не нужны тяжелые инструментальные тесты, и достаточно простых тестов джунтов. Я использую хороший фреймворк Robolectric для этой цели.

В Gradle добавить:

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 не являются обязательными, но я нашел их очень полезными, поэтому я настоятельно рекомендую включить их тоже.

Затем в Варианты сборки указать Модульные тесты как Тестовый артефакт : enter image description here

Теперь пришло время написать несколько реальных тестов :-) В качестве примера возьмем стандартный пример проекта «Пустое действие с фрагментом».

Я добавил несколько строк кода, чтобы на самом деле было что проверить:

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

И класс Корова:

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

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

Тестовый набор Robolectic будет выглядеть примерно так:

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 () или сделать это прямо из теста.

NB! Я не потратил слишком много времени на это, но похоже, что почти невозможно связать его вместе с Dagger (я не знаю, проще ли это с Dagger2), так как вы не можете установить пользовательское тестовое приложение с ложными инъекциями.

2. Инструментальное тестирование фрагментов

Сложность этого подхода сильно зависит от того, используете ли вы инъекцию Dagger / Dependency в приложении, которое вы хотите протестировать.

В Варианты сборки указать Инструментальные тесты Android как Тестовый артефакт : enter image description here

В 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"
    }
    .....
}

(опять же, почти все они не обязательны, но они могут сделать вашу жизнь намного проще)

- Если у вас нет Кинжала

Это счастливый путь. Отличие от Robolectric от вышеупомянутого будет лишь в мелких деталях.

Предварительный шаг 1 : Если вы собираетесь использовать Mockito, вы должны включить его на устройствах и эмуляторах с помощью этого хака:

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 остается тем же, что и выше. Таким образом, набор тестов будет выглядеть так:

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

}

Как видите, тестовый класс является расширением ActivityInstrumentationTestCase2 класса. Кроме того, очень важно обратить внимание на метод startFragment , который изменился по сравнению с примером JUnit: по умолчанию тесты не выполняются в потоке пользовательского интерфейса, и нам необходимо явно вызывать выполнение в ожидании транзакций FragmentManager.

- Если у вас есть Кинжал

Здесь все становится серьезно: -)

Во-первых, мы избавляемся от ActivityInstrumentationTestCase2 в пользу ActivityUnitTestCase класса, как базового класса для всех тестовых классов фрагмента.

Как обычно, это не так просто, и есть несколько ловушек ( этот является одним из примеров). Таким образом, нам нужно увеличить наше AcitivityUnitTestCase до ActivityUnitTestCaseOverride

Это слишком долго, чтобы публиковать его полностью здесь, поэтому я загружаю его полную версию в 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 для всех ваших тестов фрагментов:

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

Здесь есть несколько важных вещей.

1) Мы переопределяем метод setActivity () , чтобы установить тему AppCompact для действия. Без этого тестовый костюм рухнет.

2) Метод setUpActivityAndFragment ():

I. создает действие (=> getActivity () начинает возвращать ненулевое значение в тестах и ​​в тестируемом приложении) 1) onCreate () вызываемой активности;

2) onStart () вызванной активности;

3) onResume () вызываемой активности;

II. присоединить и запустить фрагмент к активности

1) onAttach () фрагмента вызывается;

2) onCreateView () вызванного фрагмента;

3) onStart () вызванного фрагмента;

4) onResume () вызванного фрагмента;

3) Метод createMockApplication (): Как и в версии без кинжала, в предварительном шаге 1 мы включаем насмешку на устройствах и на эмуляторах.

Затем мы заменяем обычное приложение его инъекциями на наше пользовательское 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;
    }
.........
}

т.е. вместо реальных классов мы предоставляем фрагментам их издевательские версии. (Это легко отследить, позволяет настроить результаты вызовов методов и т. Д.).

А 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);
    }
    ....
}

Вот и все! Теперь у вас включены инструментальные / JUnit-тесты для ваших фрагментов.

Я искренне надеюсь, что этот пост кому-нибудь поможет.

17 голосов
/ 18 апреля 2013

Предположим, у вас есть класс FragmentActivity MyFragmentActivity, в который добавлен общедоступный класс Fragment MyFragment с использованием FragmentTransaction.Просто создайте класс 'JUnit Test Case', который расширяет 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
    }
}

Надеюсь, это поможет.Примите мой ответ, если вы найдете это полезным.Спасибо.

0 голосов
/ 24 апреля 2015

Добавление к ответу @ abhijit.mitkar.

Учитывая сценарий, что ваш фрагмент не является публичным участником в тестируемой активности.

protected void setUp() {
   mActivity = getActivity();
   mFragment = new TheTargetFragment();

   FragmentTransaction transaction = mActivity.getSupportFragmentManager().beginTransaction();
   transaction.add(R.id.fragment_container, mFragment, "FRAGMENT_TAG");
   transaction.commit();
}

Цель приведенного выше кодазаменить фрагмент новым объектом фрагмента, к которому у нас есть доступ.

Приведенный ниже код позволит вам получить доступ к элементам пользовательского интерфейса фрагментов.

TextView randomTextView= (TextView) mFragment.getView().findViewById(R.id.textViewRandom);

Получение пользовательского интерфейса издействие не даст ожидаемого результата.

TextView randomTextView= (TextView) mActivity.findViewById(R.id.textViewRandom);

Наконец, если вы хотите внести некоторые изменения в пользовательский интерфейс.Как хороший Android-разработчик, делайте это в главном потоке.

mActivity.runOnUiThread(new Runnable() {
    @Override
    public void run() {
        // set text view's value
    }
});

Примечание: Возможно, вы захотите давать ему Thread.sleep () каждый раз, когда заканчивается тест.Чтобы избежать блокировки, getInstrumentation (). WaitForIdleSync ();кажется, не всегда работает.

Я использовал ActivityInstrumentationTestCase2 , так как я проводил функциональное тестирование.

0 голосов
/ 20 ноября 2011

Я почти уверен, что вы можете делать то, что говорите, создавать фиктивное задание и тестировать фрагмент оттуда. Вам просто нужно экспортировать библиотеку совместимости в основной проект, и вы сможете получить доступ к фрагментам из тестового проекта. Я собираюсь создать пример проекта и протестировать код здесь, и обновлю свой ответ на основе того, что я узнаю.

Для получения более подробной информации об экспорте библиотеки совместимости, проверьте здесь .

...