Подтвердить что ты не робот

Unit Test фрагмент Android

Я хочу unit test класс Android Fragment.

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

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

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

Как я понимаю, фрагмент должен жить внутри Activity. Могу ли я создать mock Activity или получить приложение или контекст для обеспечения Activity, в котором я могу проверить свой фрагмент?

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

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

Трев

4b9b3361

Ответ 1

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

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

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

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

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

Затем в вариантах сборки укажите Unit Tests в качестве тестового артефакта: введите описание изображения здесь

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

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

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 будет выглядеть примерно так:

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! Я не потратил на это слишком много времени, но похоже, что почти невозможно связать его вместе с кинжалом (я не знаю, проще ли это с Dagger2), так как вы не можете установить специальное тестовое приложение с издеваемыми инъекциями.

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

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

В вариантах сборки укажите Инструментальные тесты Android в качестве тестового артефакта: введите описание изображения здесь

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

}

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

- Если у вас есть Dagger

Здесь все серьезно: -)

Во-первых, мы избавляемся от 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():

я. создает активность (= > 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 - это просто ваше собственное расширение приложения, которое должно поддерживать установку модулей и инициализацию 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);
    }
    ....
}

Что это! Теперь у вас есть тесты Instrumental/JUnit для ваших фрагментов.

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

Ответ 2

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

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

Ответ 3

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

Подробнее о том, как экспортировать библиотеку совместимости, см. здесь.

Ответ 4

Добавление в ответ @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 value
    }
});

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

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