Я боролся с тем же вопросом. Тем более, что большинство примеров кода уже устарели + 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 не являются обязательными, но я нашел их очень полезными, поэтому я настоятельно рекомендую включить их тоже.
Затем в Варианты сборки указать Модульные тесты как Тестовый артефакт :
Теперь пришло время написать несколько реальных тестов :-)
В качестве примера возьмем стандартный пример проекта «Пустое действие с фрагментом».
Я добавил несколько строк кода, чтобы на самом деле было что проверить:
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 как Тестовый артефакт :
В 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-тесты для ваших фрагментов.
Я искренне надеюсь, что этот пост кому-нибудь поможет.