Виктор Мацкевич

Асинхронные задачи в приложении и Android Instrumentation Test.
Доступные решения

В настоящее время практически во всех приложениях присутствуют асинхронные задачи. Яркий пример тому - запросы к серверу.

Рассмотрим следующий случай. Модернизируем наш калькулятор из прошлой статьи. Представим, что по нажатию на кнопку «Calculate» выполняется запрос, ответ которого будет являться результатом вычисления. Для этого переделаем обработчик нажатия кнопки «Calculate».

Обвернём метод calculate() классa MainActivity.java в AsyncTask:
AsyncTask мы используем для имитации обычного запроса на сервер.

После чего напишем новый тест для проверки функции calculate().
И снова запустим наш тест.
Тест не пройден. Почему? На картинке ниже наглядно продемонстрировано, что пошло не так:
Напомним, что метод calculate() теперь вызывается в методе doInBackground() класса AsyncTask. Данный метод вызывается не в основном потоке, а инструмент getInstrumentation()
.waitForIdleSync()
ждёт пока очередь у основного потока будет не пустой. Вследствие чего возникает проблема в том, что проверка результата происходит раньше, чем все потоки синхронизируются. Что нас, конечно же, не устраивает.

В связи с этим возникает потребность осуществлять искусственную синхронизацию для корректного прохождения теста.

Сделать это можно следующими способами:


  • getInstrumentation()
    .waitForIdleSync()
  • Synchronized
waitForIdleSync()
В прошлой статье шла речь об этой функции. Кратко напомню: она ждёт, пока все UI потоки завершат свою работу. Как же нам можно применить её, если созданный нами поток не является UI потоком?

Всё очень просто. Нам необходимо добавить анимированный объект, анимация которого завершится после выполнения функции calculate(). Самый очевидный вариант - добавить в наш layout ProgressBar.

Немного перестроим структуру разметки. Ниже представлена новая разметка:
А также немного модернизируем наш код. Теперь он выглядит вот так.

После чего прогоняем заново тест. Отлично, тест пройден!
Получилась следующая ситуация. Когда мы отобразили ProgressBar, включилась его анимация. Все действия, связанные с представлением, являются UI потоком. Собственно поэтому и функция getInstrumentation()
.waitForIdleSync()
находится в ожидании.

Но у такого метода есть огромный минус. Иногда необходимо убрать анимацию на тестируемом устройстве для проверки. Если мы выключим анимацию, то, собственно, ожидания не будет. Именно поэтому мы сейчас рассмотрим второй случай, связанный с синхронизацией потоков.
Synchronized
Суть приёма заключается в следующем: мы создаём объект и, применив конструкцию synchronized(Object) {}, останавливаем тест, пока наш объект не вызовет функцию notify(). Данную функцию он вызовет, когда сработает метод коллбека.

Для реализации данного приёма необходимо выполнить следующие шаги:


  1. Написать интерфейс для обратной связи
  2. Создать экземпляр этого интерфейса в тесте и передать его активити, переопределить его и установить обращение к его методам
  3. Добавить точку синхронизации в тесте
Начнём с написания интерфейса. Выглядеть он будет следующим образом:
Идея заключается в том, что метод calculateIsDone() вызовется после выполнения расчёта.

В классе MainActivity.java объявляем переменную mMainActivityCallBack, она является экземпляром интерфейса MainActivityCallBack.java, и создаём setter для этой переменной:
Но на этом не всё, нам ещё необходимо добавить вызов метода calculateIsDone(). Для этого переходим в метод calculate() и дописываем после выполнения расчёта следующее:
Перейдем теперь к классу MainActivityTest.java. Добавим следующий фрагмент кода перед вызовом функции calculateButton.performClick():
Здесь мы просто создали объект для синхронизации и изменили callback для activity. В переопределенном методе, вызвав функцию notify(), мы синхронизируем объект syncObject.

Также стоит добавить следующий фрагмент после вызова метода calculateButton.performClick():
Это наша точка синхронизации, метод wait() ждет, пока не выполнится метод notify(). Более подробно этот процесс мы можем увидеть на рисунке ниже.
После чего запускаем наш тест и видим, что тест пройден.
Вот таким незамысловатым способом нам удалось решить проблему асинхронных задач.

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