Учебное пособие по тестированию Android: модульное тестирование как настоящий зеленый дроид

Опубликовано: 2022-03-11

Как опытные разработчики приложений, по мере взросления разрабатываемых нами приложений мы чувствуем, что пора начинать тестирование. Бизнес-правила часто подразумевают, что система должна обеспечивать стабильность в различных выпусках. В идеале мы также хотим автоматизировать процесс сборки и автоматически публиковать приложение. Для этого нам нужны инструменты тестирования Adnroid, чтобы гарантировать, что сборка работает должным образом.

Тесты могут обеспечить дополнительный уровень уверенности в том, что мы создаем. Трудно (если не невозможно) создать идеальный продукт без ошибок. Поэтому наша цель будет заключаться в том, чтобы повысить наши шансы на успех на рынке, создав набор тестов, который быстро выявит новые ошибки в нашем приложении.

Руководство по тестированию Android

Когда дело доходит до Android и различных мобильных платформ в целом, тестирование приложений может быть проблемой. Внедрение модульных тестов и следование принципам разработки через тестирование или тому подобное часто может показаться, по крайней мере, неинтуитивным. Тем не менее, тестирование важно, и его нельзя принимать как должное или игнорировать. Дэвид, Кент и Мартин обсудили преимущества и недостатки тестирования в серии бесед между собой, собранных в статье под названием «Мертв ли ​​TDD?». Вы также можете найти там настоящие видео-беседы и получить больше информации о том, подходит ли тестирование для вашего процесса разработки и в какой степени вы могли бы включить его, начиная с сегодняшнего дня.

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

Должен ли я прочитать это?

В этом руководстве будут рассмотрены различные возможности тестирования приложений Android. Разработчики или менеджеры проектов, которые хотят лучше понять текущие возможности тестирования платформы Android, могут использовать это руководство, чтобы решить, хотят ли они использовать какой-либо из подходов, упомянутых в этой статье. Однако это не серебряная пуля, поскольку обсуждение такой темы по своей природе варьируется от продукта к продукту, а также к срокам, качеству кода кодовой базы, уровню связанности системы, предпочтениям разработчика в архитектурном дизайне, прогнозируемому сроку службы функции. тест и т.д.

Мышление единицами: Android-тестирование

В идеале мы хотим протестировать одну логическую единицу/компонент архитектуры независимо. Таким образом, мы можем гарантировать, что наш компонент работает правильно для набора входных данных, которые мы ожидаем. Зависимости можно имитировать, что позволит нам писать тесты, которые выполняются быстро. Кроме того, мы сможем моделировать различные состояния системы на основе предоставленных входных данных для теста, охватывая экзотические случаи в процессе.

Цель модульного тестирования Android — изолировать каждую часть программы и показать, что отдельные части верны. Модульный тест предоставляет строгий письменный контракт, которому должен соответствовать фрагмент кода. В результате он дает несколько преимуществ. —Википедия

Робоэлектрик

Robolectric — это среда модульного тестирования Android, которая позволяет запускать тесты внутри JVM на рабочей станции разработки. Robolectric переписывает классы Android SDK по мере их загрузки и позволяет запускать их на обычной JVM, что сокращает время тестирования. Кроме того, он обрабатывает инфляцию представлений, загрузку ресурсов и многое другое, что реализовано в собственном коде C на устройствах Android, что делает необходимость в эмуляторах и физических устройствах для запуска автоматических тестов устаревшей.

Мокито

Mockito — это мок-фреймворк, который позволяет нам писать чистые тесты на java. Это упрощает процесс создания тестовых двойников (моков), которые используются для замены исходных зависимостей компонента/модуля, используемых в производстве. Ответ StackOverflow обсуждает различия между макетами и заглушками в довольно простых терминах, которые вы можете прочитать, чтобы узнать больше.

 // you can mock concrete classes, not only interfaces LinkedList mockedList = mock(LinkedList.class); // stubbing appears before the actual execution when(mockedList.get(0)).thenReturn("first"); // the following prints "first" System.out.println(mockedList.get(0)); // the following prints "null" because get(999) was not stubbed System.out.println(mockedList.get(999));

Кроме того, с Mockito мы можем проверить, был ли вызван метод:

 // mock creation List mockedList = mock(List.class); // using mock object - it does not throw any "unexpected interaction" exception mockedList.add("one"); mockedList.clear(); // selective, explicit, highly readable verification verify(mockedList).add("one"); verify(mockedList).clear(); 

Тестдроид

Теперь мы знаем, что можем указать пары действие-реакция, которые определяют, что происходит, когда мы выполняем определенное действие над фиктивным объектом/компонентом. Таким образом, мы можем издеваться над целыми модулями нашего приложения и для каждого теста заставлять издевательский модуль реагировать по-разному. Различные способы будут отражать возможные состояния пары тестируемого компонента и имитируемого компонента.

Модульное тестирование

В этом разделе мы будем использовать архитектуру MVP (Model View Presenter). Активности и фрагменты — это представления, модели — уровень репозитория для обращений к базе данных или удаленным службам, а презентатор — «мозг», который связывает все это вместе, реализуя определенную логику для управления представлениями, моделями и потоком данных через заявление.

Абстрагирование компонентов

Насмешливые представления и модели

В этом примере тестирования Android мы создадим имитацию представлений, моделей и компонентов репозитория, а также проведем модульное тестирование презентатора. Это один из самых маленьких тестов, нацеленный на один компонент в архитектуре. Кроме того, мы будем использовать заглушку метода для создания правильной, проверяемой цепочки реакций:

 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterTest { private Calendar cal = Calendar.getInstance(); @Mock private IFitnessListModel model; @Mock private IFitnessListView view; private IFitnessListPresenter presenter; @Before public void setup() { MockitoAnnotations.initMocks(this); final FitnessEntry entryMock = mock(FitnessEntry.class); presenter = new FitnessListPresenter(view, model); /* Define the desired behaviour. Queuing the action in "doAnswer" for "when" is executed. Clear and synchronous way of setting reactions for actions (stubbing). */ doAnswer((new Answer<Object>() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { ArrayList<FitnessEntry> items = new ArrayList<>(); items.add(entryMock); ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); return null; } })).when(model).fetchAllItems((IFitnessListPresenterCallback) presenter); } /** Verify if model.fetchItems was called once. Verify if view.onFetchSuccess is called once with the specified list of type FitnessEntry The concrete implementation of ((IFitnessListPresenterCallback) presenter).onFetchAllSuccess(items); calls the view.onFetchSuccess(...) method. This is why we verify that view.onFetchSuccess is called once. */ @Test public void testFetchAll() { presenter.fetchAllItems(false); // verify can be called only on mock objects verify(model, times(1)).fetchAllItems((IFitnessListPresenterCallback) presenter); verify(view, times(1)).onFetchSuccess(new ArrayList<>(anyListOf(FitnessEntry.class))); } }

Насмешка над глобальным сетевым уровнем с помощью MockWebServer

Часто удобно иметь возможность имитировать глобальный сетевой уровень. MockWebServer позволяет нам ставить в очередь ответы на определенные запросы, которые мы выполняем в наших тестах. Это дает нам возможность имитировать неясные ответы, которые мы ожидаем от сервера, но которые не так просто воспроизвести. Это позволяет нам обеспечить полное покрытие при написании небольшого дополнительного кода.

Репозиторий кода MockWebServer предоставляет отличный пример, к которому вы можете обратиться для лучшего понимания этой библиотеки.

Пользовательские тестовые двойники

Вы можете написать свою собственную модель или компонент репозитория и внедрить его в тест, предоставив другой модуль графу объектов с помощью Dagger (http://square.github.io/dagger/). У нас есть возможность проверить, правильно ли было обновлено состояние представления на основе данных, предоставленных компонентом макета модели:

 /** Custom mock model class */ public class FitnessListErrorTestModel extends FitnessListModel { // ... @Override public void fetchAllItems(IFitnessListPresenterCallback callback) { callback.onError(); } @Override public void fetchItemsInRange(final IFitnessListPresenterCallback callback, DateFilter filter) { callback.onError(); } }
 @RunWith(RobolectricTestRunner.class) @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18) public class FitnessListPresenterDaggerTest { private FitnessActivity activity; private FitnessListFragment fitnessListFragment; @Before public void setup() { /* setupActivity runs the Activity lifecycle methods on the specified class */ activity = Robolectric.setupActivity(FitnessActivity.class); fitnessListFragment = activity.getFitnessListFragment(); /* Create the objectGraph with the TestModule */ ObjectGraph localGraph = ObjectGraph.create(TestModule.newInstance(fitnessListFragment)); /* Injection */ localGraph.inject(fitnessListFragment); localGraph.inject(fitnessListFragment.getPresenter()); } @Test public void testInteractorError() { fitnessListFragment.getPresenter().fetchAllItems(false); /* suppose that our view shows a Toast message with the specified text below when an error is reported, so we check for it. */ assertEquals(ShadowToast.getTextOfLatestToast(), "Something went wrong!"); } @Module( injects = { FitnessListFragment.class, FitnessListPresenter.class }, overrides = true, library = true ) static class TestModule { private IFitnessListView view; private TestModule(IFitnessListView view){ this.view = view; } public static TestModule newInstance(IFitnessListView view){ return new TestModule(view); } @Provides public IFitnessListInteractor provideFitnessListInteractor(){ return new FitnessListErrorTestModel(); } @Provides public IFitnessListPresenter provideFitnessPresenter(){ return new FitnessListPresenter(view); } } }

Запуск тестов

Android-студия

Вы можете легко щелкнуть правой кнопкой мыши тестовый класс, метод или весь тестовый пакет и запустить тесты из диалогового окна параметров в среде IDE.

Терминал

Запуск тестов Android-приложений из терминала создает отчеты для протестированных классов в папке «build» целевого модуля. Более того, если вы планируете настроить автоматизированный процесс сборки, вы будете использовать терминальный подход. С Gradle вы можете запускать все тесты с отладкой, выполнив следующее:

 gradle testDebug

Доступ к исходному набору «test» из версии Android Studio

Версия 1.1 Android Studio и плагин Android Gradle обеспечивают поддержку модульного тестирования вашего кода. Вы можете узнать больше, прочитав их прекрасную документацию по нему. Эта функция является экспериментальной, но также является отличным дополнением, поскольку теперь вы можете легко переключаться между своими модульными тестами и наборами исходных кодов инструментальных тестов из среды IDE. Он ведет себя так же, как если бы вы переключали вкусы в IDE.

Модульное тестирование Android

Облегчение процесса

Написание тестов приложений для Android может быть не таким увлекательным, как разработка оригинального приложения. Следовательно, некоторые советы о том, как облегчить процесс написания тестов и избежать распространенных проблем при настройке проекта, помогут на долгое время.

AssertJ Android

AssertJ Android, как вы могли догадаться из названия, представляет собой набор вспомогательных функций, созданных специально для Android. Это расширение популярной библиотеки AssertJ. Функциональность, предоставляемая AssertJ Android, варьируется от простых утверждений, таких как «assertThat(view).isGone()», до таких сложных вещей, как:

 assertThat(layout).isVisible() .isVertical() .hasChildCount(4) .hasShowDividers(SHOW_DIVIDERS_MIDDLE)

AssertJ Android и его расширяемость гарантируют простую и хорошую отправную точку для написания тестов для приложений Android.

Robolectric и манифестный путь

При использовании Robolectric вы можете заметить, что вам нужно указать местоположение манифеста и что версия SDK установлена ​​​​на 18. Вы можете сделать это, включив аннотацию «Config».

 @Config(manifest = "app/src/main/AndroidManifest.xml", emulateSdk = 18)

Выполнение тестов, требующих Robolectric от терминала, может привести к новым проблемам. Например, вы можете увидеть такие исключения, как «Тема не установлена». Если тесты выполняются правильно из IDE, но не из терминала, возможно, вы пытаетесь запустить их из пути в терминале, где указанный путь манифеста не может быть разрешен. Жестко закодированное значение конфигурации для пути манифеста может не указывать на правильное расположение с точки выполнения команды. Это можно решить с помощью пользовательских бегунов:

 public class RobolectricGradleTestRunner extends RobolectricTestRunner { public RobolectricGradleTestRunner(Class<?> testClass) throws InitializationError { super(testClass); } @Override protected AndroidManifest getAppManifest(Config config) { String appRoot = "../app/src/main/"; String manifestPath = appRoot + "AndroidManifest.xml"; String resDir = appRoot + "res"; String assetsDir = appRoot + "assets"; AndroidManifest manifest = createAppManifest(Fs.fileFromPath(manifestPath), Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)); return manifest; } }

Конфигурация Gradle

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

 // Robolectric testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.9.5' testCompile 'com.squareup.dagger:dagger:1.2.2' testProvided 'com.squareup.dagger:dagger-compiler:1.2.2' testCompile 'com.android.support:support-v4:21.0.+' testCompile 'com.android.support:appcompat-v7:21.0.3' testCompile('org.robolectric:robolectric:2.4') { exclude module: 'classworlds' exclude module: 'commons-logging' exclude module: 'httpclient' exclude module: 'maven-artifact' exclude module: 'maven-artifact-manager' exclude module: 'maven-error-diagnostics' exclude module: 'maven-model' exclude module: 'maven-project' exclude module: 'maven-settings' exclude module: 'plexus-container-default' exclude module: 'plexus-interpolation' exclude module: 'plexus-utils' exclude module: 'wagon-file' exclude module: 'wagon-http-lightweight' exclude module: 'wagon-provider-api' }

Robolectric и игровые сервисы

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

 <meta-data android:name="com.google.android.gms.version" android:value="@integer/gms_version" tools:replace="android:value" />

Зависимости Robolectric для поддержки библиотек

Еще одна интересная проблема тестирования заключается в том, что Robolectric не может правильно ссылаться на вспомогательные библиотеки. Решение состоит в том, чтобы добавить файл «project.properties» в модуль, где находятся тесты. Например, для библиотек Support-v4 и AppCompat файл должен содержать:

 android.library.reference.1=../../build/intermediates/exploded-aar/com.android.support/support-v4/21.0.3 android.library.reference.2=../../build/intermediates/exploded-aar/com.android.support/appcompat-v7/21.0.3

Приемочное/регрессионное тестирование

Приемочное/регрессионное тестирование автоматизирует часть заключительного этапа тестирования в реальной среде Android на 100 %. На этом уровне мы не используем имитационные классы ОС Android — тесты выполняются на реальных устройствах и эмуляторах.

Приемочное и регрессионное тестирование Android

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

Немного сложно создать правильный тест, который проходит на широком спектре устройств, но, как всегда, вы должны мечтать о большем, но начинать с малого. Создание тестов с помощью Robotium — итеративный процесс. С помощью нескольких трюков его можно значительно упростить.

Роботиум

Robotium — это среда автоматизации тестирования Android с открытым исходным кодом, которая существует с января 2010 года. Стоит отметить, что Robotium — это платное решение, но с бесплатной пробной версией.

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

Testdroid Recorder — это бесплатный регистратор тестов, который создает тесты Robotium, записывая клики, которые вы выполняете в пользовательском интерфейсе. Установить инструмент очень просто, как описано в их документации, сопровождаемой пошаговым видео.

Поскольку Testdroid Recorder является подключаемым модулем Eclipse, а в этой статье мы имеем в виду Android Studio, в идеале это должно быть поводом для беспокойства. Однако в данном случае это не проблема, так как вы можете использовать плагин напрямую с APK и записывать тесты против него.

Создав тесты, вы можете скопировать и вставить их в Android Studio вместе с любой зависимостью, которая требуется для рекордера Testdroid, и все готово. Записанный тест будет выглядеть примерно так:

 public class LoginTest extends ActivityInstrumentationTestCase2<Activity> { private static final String LAUNCHER_ACTIVITY_CLASSNAME = "com.toptal.fitnesstracker.view.activity.SplashActivity"; private static Class<?> launchActivityClass; static { try { launchActivityClass = Class.forName(LAUNCHER_ACTIVITY_CLASSNAME); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } private ExtSolo solo; @SuppressWarnings("unchecked") public LoginTest() { super((Class<Activity>) launchActivityClass); } // executed before every test method @Override public void setUp() throws Exception { super.setUp(); solo = new ExtSolo(getInstrumentation(), getActivity(), this.getClass() .getCanonicalName(), getName()); } // executed after every test method @Override public void tearDown() throws Exception { solo.finishOpenedActivities(); solo.tearDown(); super.tearDown(); } public void testRecorded() throws Exception { try { assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_username_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_username_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_username_input"), "[email protected]"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for edit text (id: com.toptal.fitnesstracker.R.id.login_password_input) failed.", solo.waitForEditTextById( "com.toptal.fitnesstracker.R.id.login_password_input", 20000)); solo.enterText( (EditText) solo .findViewById("com.toptal.fitnesstracker.R.id.login_password_input"), "123456"); solo.sendKey(ExtSolo.ENTER); solo.sleep(500); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); solo.clickOnButton((Button) solo .findViewById("com.toptal.fitnesstracker.R.id.parse_login_button")); assertTrue("Wait for text fitness list activity.", solo.waitForActivity(FitnessActivity.class)); assertTrue("Wait for text KM.", solo.waitForText("KM", 20000)); /* Custom class that enables proper clicking of ActionBar action items */ TestUtils.customClickOnView(solo, R.id.action_logout); solo.waitForDialogToOpen(); solo.waitForText("OK"); solo.clickOnText("OK"); assertTrue("waiting for ParseLoginActivity after logout", solo.waitForActivity(ParseLoginActivity.class)); assertTrue( "Wait for button (id: com.toptal.fitnesstracker.R.id.parse_login_button) failed.", solo.waitForButtonById( "com.toptal.fitnesstracker.R.id.parse_login_button", 20000)); } catch (AssertionFailedError e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } catch (Exception e) { solo.fail( "com.example.android.apis.test.Test.testRecorded_scr_fail", e); throw e; } } }

Если вы внимательно посмотрите, то заметите, что большая часть кода довольно прямолинейна.

При записи тестов не скупитесь на операторы ожидания. Подождите, пока появятся диалоги, появятся действия, появятся тексты. Это гарантирует, что действие и иерархия представлений готовы к взаимодействию, когда вы выполняете действие на текущем экране. При этом делать скриншоты. Автоматические тесты обычно проходят без присмотра, и скриншоты — это один из способов увидеть, что на самом деле произошло во время этих тестов.

Независимо от того, проходят ли тесты или нет, отчеты — ваши лучшие друзья. Вы можете найти их в каталоге сборки «module/build/outputs/reports»:

отчет об испытаниях

Теоретически команда контроля качества могла бы записывать тесты и оптимизировать их. Это можно сделать, приложив усилия к стандартизированной модели оптимизации тестовых случаев. Когда вы обычно записываете тесты, вам всегда нужно настроить пару вещей, чтобы все работало безупречно.

Наконец, чтобы запустить эти тесты из Android Studio, вы можете выбрать их и запустить, как если бы вы запускали модульные тесты. С терминала это однострочный:

 gradle connectedAndroidTest

Производительность тестирования

Модульное тестирование Android с помощью Robolectric выполняется очень быстро, поскольку выполняется непосредственно в JVM на вашем компьютере. По сравнению с этим приемочное тестирование на эмуляторах и физических устройствах происходит намного медленнее. В зависимости от размера потоков, которые вы тестируете, это может занять от нескольких секунд до нескольких минут на тестовый пример. Фаза приемочного тестирования должна использоваться как часть автоматизированного процесса сборки на сервере непрерывной интеграции.

Скорость можно повысить за счет распараллеливания на нескольких устройствах. Ознакомьтесь с этим замечательным инструментом от Джейка Уортона и ребят из Square http://square.github.io/spoon/. У него тоже есть хорошие репортажи.

Вынос

Существует множество доступных инструментов тестирования Android, и по мере развития экосистемы процесс настройки тестируемой среды и написания тестов будет становиться проще. Есть еще много проблем, которые нужно решить, и с широким сообществом разработчиков, работающих над ежедневными проблемами, есть много места для конструктивных обсуждений и быстрой обратной связи.

Используйте подходы, описанные в этом руководстве по тестированию Android, чтобы решить стоящие перед вами задачи. Если и когда вы столкнетесь с проблемами, вернитесь к этой статье или ссылкам, указанным в ней, для решения известных проблем.

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