Эта практическая работа входит в курс «Advanced Android in Kotlin». Вы получите максимальную пользу от этого курса, выполняя задания последовательно, но это не обязательно. Все практическая работа курса перечислены на целевой странице практической работы «Advanced Android in Kotlin» .
Введение
Когда вы реализовали первую функцию своего первого приложения, вы, вероятно, запустили код, чтобы убедиться, что он работает так, как ожидалось. Вы провели тестирование , пусть и ручное . По мере добавления и обновления функций вы, вероятно, также продолжали запускать код и проверять его работоспособность. Но делать это вручную каждый раз утомительно, подвержено ошибкам и не масштабируется.
Компьютеры отлично масштабируются и автоматизируются! Поэтому разработчики в крупных и мелких компаниях пишут автоматизированные тесты , которые запускаются программным обеспечением и не требуют ручного управления приложением для проверки работоспособности кода.
В этой серии практических занятий вы узнаете, как создать набор тестов (известный как набор тестов) для реального приложения.
В этой первой лабораторной работе рассматриваются основы тестирования на Android, вы напишете свои первые тесты и узнаете, как тестировать LiveData
и ViewModel
.
Что вам уже следует знать
Вам должно быть знакомо:
- Язык программирования Kotlin
- Следующие основные библиотеки Android Jetpack:
ViewModel
иLiveData
- Архитектура приложения, соответствующая шаблону из Руководства по архитектуре приложений и практических занятий по основам Android.
Чему вы научитесь
Вы узнаете о следующих темах:
- Как писать и запускать модульные тесты на Android
- Как использовать разработку через тестирование
- Как выбрать инструментальные тесты и локальные тесты
Вы узнаете о следующих библиотеках и концепциях кода:
- JUnit4
- Хамкрест
- Тестовая библиотека AndroidX
- Базовая тестовая библиотека компонентов архитектуры AndroidX
Что ты будешь делать?
- Настраивайте, запускайте и интерпретируйте локальные и инструментированные тесты в Android.
- Написание модульных тестов в Android с использованием JUnit4 и Hamcrest.
- Напишите простые тесты
LiveData
иViewModel
.
В этой серии практических занятий вы будете работать с приложением TO-DO Notes. Оно позволяет записывать задачи и отображать их в виде списка. Вы можете отмечать их как выполненные или нет, фильтровать или удалять.
Это приложение написано на Kotlin, имеет несколько экранов, использует компоненты Jetpack и следует архитектуре из руководства по архитектуре приложений . Научившись тестировать это приложение, вы сможете тестировать приложения, использующие те же библиотеки и архитектуру.
Чтобы начать, скачайте код:
В качестве альтернативы вы можете клонировать репозиторий Github для кода:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout starter_code
В этом задании вы запустите приложение и изучите кодовую базу.
Шаг 1: Запустите пример приложения
После загрузки приложения TO-DO откройте его в Android Studio и запустите. Оно должно скомпилироваться. Чтобы изучить приложение, выполните следующие действия:
- Создайте новую задачу с помощью плавающей кнопки действия «плюс». Сначала введите название, затем дополнительную информацию о задаче. Сохраните её, нажав зелёную галочку FAB.
- В списке задач нажмите на название только что выполненной задачи и просмотрите экран с подробными сведениями об этой задаче, чтобы увидеть остальную часть описания.
- В списке или на экране сведений установите флажок для этой задачи, чтобы установить ее статус на «Завершено» .
- Вернитесь на экран задач, откройте меню фильтров и отфильтруйте задачи по статусу «Активно» и «Завершено» .
- Откройте панель навигации и нажмите «Статистика» .
- Вернитесь на экран обзора и в меню панели навигации выберите «Очистить выполненные» , чтобы удалить все задачи со статусом «Выполнено».
Шаг 2: Изучите пример кода приложения
Приложение TO-DO основано на популярном примере архитектуры и тестирования Architecture Blueprints (с использованием версии примера с реактивной архитектурой ). Архитектура приложения соответствует руководству по архитектуре приложения . Оно использует ViewModels с фрагментами, репозиторием и Room. Если вы знакомы с каким-либо из примеров ниже, то это приложение имеет похожую архитектуру:
- Комната с видом. Codelab
- Обучающие практикумы по основам Android Kotlin
- Продвинутые обучающие практикумы по Android
- Образец Android Sunflower
- Разработка приложений для Android на Kotlin. Учебный курс Udacity
Важнее понимать общую архитектуру приложения, чем глубоко понимать логику на каком-либо одном уровне.
Вот список пакетов, которые вы найдете:
Пакет: | |
| Экран добавления или редактирования задачи: код слоя пользовательского интерфейса для добавления или редактирования задачи. |
| Уровень данных: отвечает за уровень данных задач. Он содержит код базы данных, сети и репозитория. |
| Экран статистики: код слоя пользовательского интерфейса для экрана статистики. |
| Экран сведений о задаче: код слоя пользовательского интерфейса для одной задачи. |
| Экран задач: код слоя пользовательского интерфейса для списка всех задач. |
| Вспомогательные классы: общие классы, используемые в различных частях приложения, например, для макета обновления свайпом, используемого на нескольких экранах. |
Уровень данных (.data)
Это приложение включает в себя смоделированный сетевой уровень в удалённом пакете и уровень базы данных в локальном пакете. Для простоты в этом проекте сетевой уровень смоделирован только с помощью HashMap
с задержкой, а не с помощью реальных сетевых запросов.
DefaultTasksRepository
координирует или выступает посредником между сетевым уровнем и уровнем базы данных и возвращает данные на уровень пользовательского интерфейса.
Уровень пользовательского интерфейса (.addedittask, .statistics, .taskdetail, .tasks)
Каждый пакет слоя пользовательского интерфейса содержит фрагмент и модель представления, а также любые другие классы, необходимые для пользовательского интерфейса (например, адаптер для списка задач). TaskActivity
— это активность, содержащая все фрагменты.
Навигация
Навигация в приложении управляется компонентом Navigation . Он определён в файле nav_graph.xml
. Навигация активируется в моделях представления с помощью класса Event
; модели представления также определяют, какие аргументы следует передавать. Фрагменты отслеживают Event
и осуществляют фактическую навигацию между экранами.
В этом задании вы проведете свои первые тесты.
- В Android Studio откройте панель «Проект» и найдите следующие три папки:
-
com.example.android.architecture.blueprints.todoapp
-
com.example.android.architecture.blueprints.todoapp (androidTest)
-
com.example.android.architecture.blueprints.todoapp (test)
Эти папки называются исходными наборами (source set ). Исходные наборы — это папки с исходным кодом вашего приложения. Исходные наборы, отмеченные зелёным цветом ( androidTest и test ), содержат ваши тесты. При создании нового проекта Android по умолчанию вы получаете следующие три исходных набора:
-
main
: содержит код вашего приложения. Этот код используется всеми различными версиями приложения, которые вы можете собрать (так называемыми вариантами сборки ). -
androidTest
: Содержит тесты, известные как инструментированные тесты. -
test
: Содержит тесты, известные как локальные тесты.
Разница между локальными испытаниями и инструментальными испытаниями заключается в способе их проведения.
Локальные тесты ( набор исходных test
)
Эти тесты запускаются локально на виртуальной машине Java (JVM) вашего компьютера, на котором ведется разработка, и не требуют эмулятора или физического устройства. Благодаря этому они выполняются быстро, но их точность ниже, а значит, они ведут себя не так, как в реальном мире.
В Android Studio локальные тесты представлены значком зеленого и красного треугольника.
Инструментированные тесты ( исходный набор androidTest
)
Эти тесты проводятся на реальных или эмулируемых устройствах Android, поэтому они отражают то, что произойдет в реальном мире, но при этом они гораздо медленнее.
В Android Studio инструментированные тесты представлены Android со значком зеленого и красного треугольника.
Шаг 1: Проведите локальный тест
- Откройте
test
папку и найдите файл ExampleUnitTest.kt . - Щелкните по нему правой кнопкой мыши и выберите Запустить ExampleUnitTest .
В окне « Выполнить » в нижней части экрана вы должны увидеть следующий вывод:
- Обратите внимание на зелёные галочки и разверните результаты теста, чтобы убедиться, что один тест с именем
addition_isCorrect
пройден. Приятно знать, что сложение работает так, как и ожидалось!
Шаг 2: Заставьте тест провалиться
Ниже представлен тест, который вы только что провели.
ExampleUnitTest.kt
// A test class is just a normal class
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
// Here you are checking that 4 is the same as 2+2
assertEquals(4, 2 + 2)
}
}
Обратите внимание, что тесты
- являются классом в одном из тестовых исходных наборов.
- содержат функции, которые начинаются с аннотации
@Test
(каждая функция — это отдельный тест). - обычно содержат утверждения.
Android использует для тестирования библиотеку JUnit (в этой практической работе — JUnit4). Как утверждения, так и аннотация @Test
взяты из JUnit.
Утверждение — это ядро вашего теста. Это оператор кода, который проверяет, что ваш код или приложение ведут себя ожидаемым образом. В данном случае это утверждение: assertEquals(4, 2 + 2)
которое проверяет, что 4 равно 2 + 2.
Чтобы увидеть, как выглядит проваленный тест, добавьте утверждение, которое легко распознать как проваленное. Оно проверит, что 3 равно 1+1.
- Добавьте
assertEquals(3, 1 + 1)
к тестуaddition_isCorrect
.
ExampleUnitTest.kt
class ExampleUnitTest {
// Each test is annotated with @Test (this is a Junit annotation)
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
assertEquals(3, 1 + 1) // This should fail
}
}
- Проведите тест.
- В результатах теста обратите внимание на значок X рядом с тестом.
- Также обратите внимание:
- Одно неверное утверждение делает весь тест недействительным.
- Вам сообщают ожидаемое значение (3) в сравнении со значением, которое было фактически рассчитано (2).
- Вы перенаправлены на строку невыполненного утверждения
(ExampleUnitTest.kt:16)
.
Шаг 3: Проведение инструментального теста
Инструментированные тесты находятся в исходном наборе androidTest
.
- Откройте исходный набор
androidTest
. - Запустите тест под названием
ExampleInstrumentedTest
.
Пример инструментального теста
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.android.architecture.blueprints.reactive",
appContext.packageName)
}
}
В отличие от локального теста, этот тест выполняется на устройстве (в примере ниже — эмулированном телефоне Pixel 2):
Если у вас подключено устройство или запущен эмулятор, вы должны увидеть запуск теста на эмуляторе.
В этом задании вы напишете тесты для getActiveAndCompleteStats
, которая вычисляет процент активных и завершённых задач в статистике вашего приложения. Эти цифры можно увидеть на экране статистики приложения.
Шаг 1: Создайте тестовый класс
- В
main
исходном наборе, вtodoapp.statistics
, откройтеStatisticsUtils.kt
. - Найдите функцию
getActiveAndCompletedStats
.
StatisticsUtils.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
data class StatsResult(val activeTasksPercent: Float, val completedTasksPercent: Float)
Функция getActiveAndCompletedStats
принимает список задач и возвращает StatsResult
. StatsResult
— это класс данных, содержащий два числа: процент выполненных задач и процент активных задач.
Android Studio предоставляет вам инструменты для создания тестовых заглушек, которые помогут вам реализовать тесты для этой функции.
- Щелкните правой кнопкой мыши
getActiveAndCompletedStats
и выберите «Создать» > «Тест» .
Откроется диалоговое окно «Создать тест» :
- Измените имя класса: на
StatisticsUtilsTest
(вместоStatisticsUtilsKtTest
; немного лучше не использовать KT в имени тестового класса). - Сохраните остальные значения по умолчанию. JUnit 4 — подходящая библиотека для тестирования. Целевой пакет указан верно (он совпадает с расположением класса
StatisticsUtils
), и вам не нужно устанавливать какие-либо флажки (это просто сгенерирует дополнительный код, но тест вы напишете с нуля). - Нажмите ОК
Откроется диалоговое окно «Выбор целевого каталога» :
Вам придётся провести локальный тест, поскольку ваша функция выполняет математические вычисления и не будет содержать специфичного для Android кода. Поэтому нет необходимости запускать её на реальном или эмулированном устройстве.
- Выберите
test
каталог (неandroidTest
), поскольку вы будете писать локальные тесты. - Нажмите ОК .
- Обратите внимание на сгенерированный класс
StatisticsUtilsTest
вtest/statistics/
.
Шаг 2: Напишите свою первую тестовую функцию
Вам предстоит написать тест, который проверит:
- если нет выполненных задач и есть одна активная задача,
- что процент активных тестов составляет 100%,
- а процент выполненных задач — 0%.
- Откройте
StatisticsUtilsTest
. - Создайте функцию с именем
getActiveAndCompletedStats_noCompleted_returnsHundredZero
.
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task
// Call your function
// Check the result
}
}
- Добавьте аннотацию
@Test
над именем функции, чтобы указать, что это тест. - Составьте список задач.
// Create an active task
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
- Вызовите
getActiveAndCompletedStats
с этими задачами.
// Call your function
val result = getActiveAndCompletedStats(tasks)
- Проверьте, соответствует ли
result
вашим ожиданиям, используя утверждения.
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
Вот полный код.
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
// Create an active task (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
}
}
- Запустите тест (щелкните правой кнопкой мыши
StatisticsUtilsTest
и выберите Выполнить ).
Должно пройти:
Шаг 3: Добавьте зависимость Hamcrest
Поскольку ваши тесты служат документацией того, что делает ваш код, приятно, когда они понятны человеку. Сравните следующие два утверждения:
assertEquals(result.completedTasksPercent, 0f)
// versus
assertThat(result.completedTasksPercent, `is`(0f))
Второе утверждение гораздо более похоже на человеческое предложение. Оно написано с использованием фреймворка для суждений Hamcrest . Ещё один хороший инструмент для написания читаемых утверждений — библиотека Truth . В этой лабораторной работе вы будете использовать Hamcrest для написания утверждений.
- Откройте
build.grade (Module: app)
и добавьте следующую зависимость.
приложение/build.gradle
dependencies {
// Other dependencies
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
}
Обычно при добавлении зависимости используется implementation
, но здесь используется testImplementation
. Когда вы готовы поделиться своим приложением со всем миром, лучше не раздувать размер APK-файла каким-либо тестовым кодом или зависимостями в вашем приложении. Вы можете указать, следует ли включать библиотеку в основной или тестовый код, с помощью конфигураций Gradle . Наиболее распространённые конфигурации:
-
implementation
— зависимость доступна во всех исходных наборах, включая тестовые исходные наборы. -
testImplementation
— зависимость доступна только в исходном наборе теста. -
androidTestImplementation
— зависимость доступна только в исходном набореandroidTest
.
Выбор конфигурации определяет, где можно использовать зависимость. Если вы напишете:
testImplementation "org.hamcrest:hamcrest-all:$hamcrestVersion"
Это означает, что Hamcrest будет доступен только в тестовом исходном наборе. Это также гарантирует, что Hamcrest не будет включён в ваше финальное приложение.
Шаг 4: Используйте Hamcrest для написания утверждений
- Обновите тест
getActiveAndCompletedStats_noCompleted_returnsHundredZero()
, чтобы использоватьassertThat
Hamcrest вместоassertEquals
.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
Обратите внимание, что при необходимости можно использовать команду import org.hamcrest.Matchers.`is`
.
Окончательный тест будет выглядеть так, как показано ниже.
StatisticsUtilsTest.kt
import com.example.android.architecture.blueprints.todoapp.data.Task
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
// Create an active tasks (the false makes this active)
val tasks = listOf<Task>(
Task("title", "desc", isCompleted = false)
)
// Call your function
val result = getActiveAndCompletedStats(tasks)
// Check the result
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
- Запустите обновленный тест, чтобы убедиться, что он все еще работает!
Эта лабораторная работа не научит вас всем тонкостям Hamcrest, поэтому, если вы хотите узнать больше, ознакомьтесь с официальным руководством .
Это факультативное задание для практики.
В этом задании вы напишете ещё больше тестов, используя JUnit и Hamcrest. Вы также будете писать тесты, используя стратегию, основанную на методологии разработки через тестирование (Test Driven Development) . Разработка через тестирование (TDD) — это школа программирования, которая гласит, что вместо написания кода функций сначала нужно написать тесты. Затем вы пишете код функций, стремясь к их прохождению.
Шаг 1. Напишите тесты
Напишите тесты для случая, когда у вас есть обычный список задач:
- Если есть одна завершенная задача и нет активных задач, процент
activeTasks
должен быть0f
, а процент завершенных задач должен быть100f
. - Если имеется две завершенные задачи и три активные задачи, процент завершения должен быть
40f
, а процент активности должен быть60f
.
Шаг 2. Напишите тест на наличие ошибки
В коде метода getActiveAndCompletedStats
есть ошибка. Обратите внимание, что он неправильно обрабатывает ситуацию, если список пуст или равен нулю. В обоих случаях оба процента должны быть равны нулю.
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
val totalTasks = tasks!!.size
val numberOfActiveTasks = tasks.count { it.isActive }
val activePercent = 100 * numberOfActiveTasks / totalTasks
val completePercent = 100 * (totalTasks - numberOfActiveTasks) / totalTasks
return StatsResult(
activeTasksPercent = activePercent.toFloat(),
completedTasksPercent = completePercent.toFloat()
)
}
Чтобы исправить код и написать тесты, вы будете использовать разработку через тестирование. Разработка через тестирование включает в себя следующие этапы.
- Напишите тест, используя структуру «Дано, Когда, Тогда» и дав имя, соответствующее соглашению.
- Подтвердите, что тест не пройден.
- Напишите минимальный код, чтобы тест прошёл.
- Повторите для всех тестов!
Вместо того, чтобы начать с исправления ошибки, вы сначала напишете тесты. Тогда вы сможете убедиться, что у вас есть тесты, которые защитят вас от случайного повторного появления этих ошибок в будущем.
- Если список пустой (
emptyList()
), то оба процента должны быть равны 0f. - Если при загрузке задач произошла ошибка, список будет
null
, а оба процента должны быть равны 0f. - Запустите тесты и убедитесь, что они не пройдены :
Шаг 3. Исправьте ошибку
Теперь, когда у вас есть тесты, исправьте ошибку.
- Исправьте ошибку в
getActiveAndCompletedStats
, вернув0f
, еслиtasks
имеет значениеnull
или пусто:
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
- Повторите тесты и убедитесь, что теперь все тесты пройдены!
Следуя принципам TDD и сначала написав тесты, вы помогли гарантировать следующее:
- Новые функциональные возможности всегда сопровождаются соответствующими тестами. Таким образом, ваши тесты служат документацией того, что делает ваш код.
- Ваши тесты проверяют правильность результатов и защищают от ошибок, которые вы уже видели.
Решение: Написание большего количества тестов
Вот все тесты и соответствующий код функций.
StatisticsUtilsTest.kt
class StatisticsUtilsTest {
@Test
fun getActiveAndCompletedStats_noCompleted_returnsHundredZero {
val tasks = listOf(
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed with an active task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 100 and 0
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_noActive_returnsZeroHundred() {
val tasks = listOf(
Task("title", "desc", isCompleted = true)
)
// When the list of tasks is computed with a completed task
val result = getActiveAndCompletedStats(tasks)
// Then the percentages are 0 and 100
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(100f))
}
@Test
fun getActiveAndCompletedStats_both_returnsFortySixty() {
// Given 3 completed tasks and 2 active tasks
val tasks = listOf(
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = true),
Task("title", "desc", isCompleted = false),
Task("title", "desc", isCompleted = false)
)
// When the list of tasks is computed
val result = getActiveAndCompletedStats(tasks)
// Then the result is 40-60
assertThat(result.activeTasksPercent, `is`(40f))
assertThat(result.completedTasksPercent, `is`(60f))
}
@Test
fun getActiveAndCompletedStats_error_returnsZeros() {
// When there's an error loading stats
val result = getActiveAndCompletedStats(null)
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
@Test
fun getActiveAndCompletedStats_empty_returnsZeros() {
// When there are no tasks
val result = getActiveAndCompletedStats(emptyList())
// Both active and completed tasks are 0
assertThat(result.activeTasksPercent, `is`(0f))
assertThat(result.completedTasksPercent, `is`(0f))
}
}
StatisticsUtils.kt
internal fun getActiveAndCompletedStats(tasks: List<Task>?): StatsResult {
return if (tasks == null || tasks.isEmpty()) {
StatsResult(0f, 0f)
} else {
val totalTasks = tasks.size
val numberOfActiveTasks = tasks.count { it.isActive }
StatsResult(
activeTasksPercent = 100f * numberOfActiveTasks / tasks.size,
completedTasksPercent = 100f * (totalTasks - numberOfActiveTasks) / tasks.size
)
}
}
Отлично справились с основами написания и запуска тестов! Далее вы научитесь писать базовые тесты ViewModel
и LiveData
.
В оставшейся части практикума вы научитесь писать тесты для двух классов Android, которые являются общими для большинства приложений — ViewModel
и LiveData
.
Вы начинаете с написания тестов для TasksViewModel
.
Вы сосредоточитесь на тестах, вся логика которых реализована в модели представления и не зависит от кода репозитория. Код репозитория включает в себя асинхронный код, базы данных и сетевые вызовы, что увеличивает сложность тестирования. Пока что вы будете избегать этого и сосредоточитесь на написании тестов для функциональности ViewModel, которые не тестируют напрямую ничего в репозитории.
Тест, который вы напишете, проверит, что при вызове метода addNewTask
срабатывает Event
открытия окна новой задачи. Вот код приложения, который вы будете тестировать.
TasksViewModel.kt
fun addNewTask() {
_newTaskEvent.value = Event(Unit)
}
Шаг 1. Создайте класс TasksViewModelTest
Следуя тем же шагам, которые вы проделали для StatisticsUtilTest
, на этом этапе вы создаете тестовый файл для TasksViewModelTest
.
- Откройте класс, который вы хотите протестировать, в пакете
tasks
TasksViewModel.
- В коде щелкните правой кнопкой мыши по имени класса
TasksViewModel
-> Generate -> Test .
- На экране «Создать тест» нажмите кнопку «ОК» , чтобы принять изменения (не нужно менять какие-либо настройки по умолчанию).
- В диалоговом окне «Выбор целевого каталога» выберите тестовый каталог.
Шаг 2. Начните писать тест ViewModel
На этом этапе вы добавляете тест модели представления, чтобы проверить, что при вызове метода addNewTask
запускается Event
открытия нового окна задачи.
- Создайте новый тест с названием
addNewTask_setsNewTaskEvent
.
TasksViewModelTest.kt
class TasksViewModelTest {
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh TasksViewModel
// When adding a new task
// Then the new task event is triggered
}
}
А как насчет контекста приложения?
При создании экземпляра TasksViewModel
для тестирования его конструктору требуется контекст приложения . Но в этом тесте вы не создаёте полноценное приложение с активностями, пользовательским интерфейсом и фрагментами. Как же получить контекст приложения?
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(???)
Библиотеки AndroidX Test включают классы и методы, предоставляющие версии компонентов, таких как приложения и активности, предназначенные для тестирования. Если у вас есть локальный тест , требующий имитации классов фреймворка Android (например, контекста приложения), выполните следующие действия для правильной настройки AndroidX Test:
- Добавьте ядро AndroidX Test и зависимости ext
- Добавьте зависимость библиотеки Robolectric Testing
- Аннотируйте класс с помощью тест-раннера AndroidJunit4
- Напишите тестовый код AndroidX
Вам предстоит выполнить эти шаги, а затем понять, что они делают вместе.
Шаг 3. Добавьте зависимости Gradle
- Скопируйте эти зависимости в файл
build.gradle
вашего модуля приложения, чтобы добавить основные зависимости AndroidX Test core и ext, а также зависимость Robolectric test.
приложение/build.gradle
// AndroidX Test - JVM testing
testImplementation "androidx.test.ext:junit-ktx:$androidXTestExtKotlinRunnerVersion"
testImplementation "androidx.test:core-ktx:$androidXTestCoreVersion"
testImplementation "org.robolectric:robolectric:$robolectricVersion"
Шаг 4. Добавьте JUnit Test Runner
- Добавьте
@RunWith(AndroidJUnit4::class)
над вашим тестовым классом.
TasksViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
Шаг 5. Используйте AndroidX Test
На этом этапе вы можете использовать библиотеку AndroidX Test. Она включает метод ApplicationProvider.getApplicationContex
t
, который получает контекст приложения.
- Создайте
TasksViewModel
с помощьюApplicationProvider.getApplicationContext()
из тестовой библиотеки AndroidX.
TasksViewModelTest.kt
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
- Вызовите
addNewTask
дляtasksViewModel
.
TasksViewModelTest.kt
tasksViewModel.addNewTask()
На этом этапе ваш тест должен выглядеть так, как показано ниже.
TasksViewModelTest.kt
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
- Запустите тест, чтобы убедиться, что он работает.
Концепция: Как работает AndroidX Test?
Что такое AndroidX Test?
AndroidX Test — это набор библиотек для тестирования. Он включает классы и методы, предоставляющие версии таких компонентов, как Applications и Activities, предназначенные для тестирования. Например, этот код, который вы написали, — пример функции AndroidX Test для получения контекста приложения.
ApplicationProvider.getApplicationContext()
Одно из преимуществ API AndroidX Test заключается в том, что они разработаны для работы как с локальными, так и с инструментированными тестами. Это удобно, потому что:
- Вы можете провести один и тот же тест как локальный или инструментальный.
- Вам не нужно изучать различные API тестирования для локальных и инструментальных тестов.
Например, поскольку вы написали код с использованием библиотек AndroidX Test, вы можете переместить класс TasksViewModelTest
из папки test
в папку androidTest
, и тесты всё равно будут выполняться. Метод getApplicationContext()
работает немного по-разному в зависимости от того, запускается ли он как локальный или инструментированный тест:
- Если это инструментированный тест, он получит фактический контекст приложения, предоставленный при загрузке эмулятора или подключении к реальному устройству.
- Если это локальный тест, он использует смоделированную среду Android.
Что такое Робоэлектрик?
Имитируемая среда Android, которую AndroidX Test использует для локальных тестов, предоставляется библиотекой Robolectric . Robolectric — это библиотека, которая создаёт имитируемую среду Android для тестов и работает быстрее, чем запуск эмулятора или работа на устройстве. Без зависимости Robolectric вы получите следующую ошибку:
Что делает @RunWith(AndroidJUnit4::class)
?
Тест-бегун — это компонент JUnit, который запускает тесты. Без инструмента для запуска тестов ваши тесты не будут запускаться. JUnit предоставляет инструмент для запуска тестов по умолчанию, который вы получаете автоматически. @RunWith
заменяет этот инструмент для запуска тестов по умолчанию.
Средство запуска тестов AndroidJUnit4
позволяет AndroidX Test запускать тесты по-разному в зависимости от того, являются ли они инструментированными или локальными.
Шаг 6. Устраните предупреждения Robolectric
При запуске кода обратите внимание, что используется Robolectric.
Благодаря AndroidX Test и тестовому исполнителю AndroidJunit4 это можно сделать без написания вами ни единой строки кода Robolectric!
Вы можете заметить два предупреждения.
-
No such manifest file: ./AndroidManifest.xml
-
"WARN: Android SDK 29 requires Java 9..."
Вы можете исправить предупреждение No such manifest file: ./AndroidManifest.xml
, обновив файл Gradle.
- Добавьте следующую строку в файл Gradle, чтобы использовать правильный манифест Android. Параметр includeAndroidResources позволяет получить доступ к ресурсам Android в ваших модульных тестах, включая файл AndroidManifest.
приложение/build.gradle
// Always show the result of every unit test when running via command line, even if it passes.
testOptions.unitTests {
includeAndroidResources = true
// ...
}
Предупреждение "WARN: Android SDK 29 requires Java 9..."
сложнее. Для выполнения тестов в Android Q требуется Java 9. Вместо того, чтобы пытаться настроить Android Studio на использование Java 9, для этой практической работы сохраните целевой вариант и скомпилируйте SDK на версии 28.
В итоге:
- Тесты чистой модели представления обычно можно включать в исходный набор
test
, поскольку их код обычно не требует использования Android. - Вы можете использовать тестовую библиотеку AndroidX для получения тестовых версий таких компонентов, как приложения и действия.
- Если вам необходимо запустить смоделированный код Android в вашем исходном
test
наборе, вы можете добавить зависимость Robolectric и аннотацию@RunWith(AndroidJUnit4::class)
.
Поздравляем! Вы используете как библиотеку AndroidX, так и Robolectric для запуска теста. Ваш тест ещё не завершён (вы ещё не написали оператор assert, он просто пишет: // TODO test LiveData
). Далее вы научитесь писать операторы assert с помощью LiveData
.
В этом задании вы научитесь правильно утверждать значение LiveData
.
Вот где вы остановились без проверки модели представления addNewTask_setsNewTaskEvent
.
TasksViewModelTest.kt
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
// TODO test LiveData
}
Для тестирования LiveData
рекомендуется сделать две вещи:
- Использовать
InstantTaskExecutorRule
- Обеспечить наблюдение
LiveData
Шаг 1. Используйте InstantTaskExecutorRule
InstantTaskExecutorRule
— это правило JUnit . При использовании его с аннотацией @get:Rule
определённый код в классе InstantTaskExecutorRule
запускается до и после тестов (чтобы увидеть точный код, воспользуйтесь сочетанием клавиш Command+B для просмотра файла).
Это правило запускает все фоновые задания, связанные с компонентами архитектуры, в одном потоке, чтобы результаты теста появлялись синхронно и в повторяющемся порядке. Используйте это правило при написании тестов, включающих тестирование LiveData!
- Добавьте зависимость gradle для базовой тестовой библиотеки Architecture Components (которая содержит это правило).
приложение/build.gradle
testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
- Открыть
TasksViewModelTest.kt
- Добавьте
InstantTaskExecutorRule
в классTasksViewModelTest
.
TasksViewModelTest.kt
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Other code...
}
Шаг 2. Добавьте класс LiveDataTestUtil.kt
Следующий шаг — убедиться, что тестируемые вами LiveData
наблюдаются.
При использовании LiveData
у вас обычно есть действие или фрагмент ( LifecycleOwner
), наблюдающий за LiveData
.
viewModel.resultLiveData.observe(fragment, Observer {
// Observer code here
})
Это наблюдение важно. Вам нужны активные наблюдатели на LiveData
, чтобы...
- запускать любые события
onChanged
. - запускать любые Трансформации .
Чтобы получить ожидаемое поведение LiveData
для LiveData
вашей модели представления, вам необходимо наблюдать LiveData
с помощью LifecycleOwner
.
Это создаёт проблему: в вашем тесте TasksViewModel
нет активности или фрагмента для наблюдения за LiveData
. Чтобы обойти это, можно использовать метод observeForever
, который обеспечивает постоянное наблюдение за LiveData
без необходимости использования LifecycleOwner
. При использовании observeForever
необходимо не забыть удалить наблюдателя , иначе возникнет риск утечки данных.
Это выглядит примерно так, как показано ниже. Взгляните на него:
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// Create observer - no need for it to do anything!
val observer = Observer<Event<Unit>> {}
try {
// Observe the LiveData forever
tasksViewModel.newTaskEvent.observeForever(observer)
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.value
assertThat(value?.getContentIfNotHandled(), (not(nullValue())))
} finally {
// Whatever happens, don't forget to remove the observer!
tasksViewModel.newTaskEvent.removeObserver(observer)
}
}
Слишком много шаблонного кода для наблюдения за одним LiveData
в тесте! Есть несколько способов избавиться от этого шаблона. Сейчас мы создадим функцию расширения LiveDataTestUtil
, чтобы упростить добавление наблюдателей.
- Создайте новый файл Kotlin с именем
LiveDataTestUtil.kt
в вашем исходном набореtest
.
- Скопируйте и вставьте код ниже.
LiveDataTestUtil.kt
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
Это довольно сложный метод. Он создаёт функцию расширения Kotlin с именем getOrAwaitValue
, которая добавляет наблюдателя, получает значение LiveData
, а затем очищает наблюдателя — по сути, это короткая, повторно используемая версия кода observeForever
, показанного выше. Полное описание этого класса см. в этой записи блога .
Шаг 3. Используйте getOrAwaitValue для записи утверждения.
На этом этапе вы используете метод getOrAwaitValue
и пишете оператор assert, который проверяет, было ли вызвано событие newTaskEvent
.
- Получите значение
LiveData
дляnewTaskEvent
с помощьюgetOrAwaitValue
.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
- Подтвердите, что значение не равно нулю.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))
Полный тест должен выглядеть как код ниже.
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.example.android.architecture.blueprints.todoapp.getOrAwaitValue
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Test
fun addNewTask_setsNewTaskEvent() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
assertThat(value.getContentIfNotHandled(), not(nullValue()))
}
}
- Запустите свой код и посмотрите тестовый проход!
Теперь, когда вы видели, как написать тест, напишите его самостоятельно. На этом этапе, используя навыки, которые вы изучили, практикуйте написание другого теста TasksViewModel
.
Шаг 1. Напишите свой собственный тест ViewModel
Вы напишете setFilterAllTasks_tasksAddViewVisible()
. Этот тест должен проверить, что если вы установили тип фильтра, чтобы показать все задачи, то кнопка «Добавить задачу» видна.
- Используя
addNewTask_setsNewTaskEvent()
для справки, напишите тест вTasksViewModelTest
называемомsetFilterAllTasks_tasksAddViewVisible()
, который устанавливает режим фильтрацииALL_TASKS
и утверждает, чтоtasksAddViewVisible
Livedata являетсяtrue
.
Используйте код ниже, чтобы начать.
Tasksviewmodeltest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
// When the filter type is ALL_TASKS
// Then the "Add task" action is visible
}
Примечание:
-
TasksFilterType
Enum для всех задач -ALL_TASKS.
- Видимость кнопки для добавления задачи контролируется TasksAddViewVisible
LiveData
tasksAddViewVisible.
- Запустите свой тест.
Шаг 2. Сравните свой тест с решением
Сравните свое решение с решением ниже.
Tasksviewmodeltest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.getOrAwaitValue(), `is`(true))
}
Проверьте, делаете ли вы следующее:
- Вы создаете свой
tasksViewModel
, используя тот же оператор AndroidxApplicationProvider.getApplicationContext()
. - Вы называете метод
setFiltering
, проходя в перечислении типа фильтраALL_TASKS
. - Вы проверяете, что
tasksAddViewVisible
верна, используя методgetOrAwaitNextValue
.
Шаг 3. Добавить правило @before
Обратите внимание, как в начале обоих ваших тестов вы определяете TasksViewModel
.
Tasksviewmodeltest
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
Когда у вас есть код настройки для нескольких тестов, вы можете использовать аннотацию @before для создания метода настройки и удалить повторный код. Поскольку все эти тесты будут протестировать TasksViewModel
и понадобится модель представления, перенесите этот код в блок @Before
.
- Создайте переменную экземпляра
lateinit
экземпляра с именемtasksViewModel|
Полем - Создайте метод с именем
setupViewModel
. - Аннотируйте это с
@Before
. - Переместите код создания модели модели в
setupViewModel
.
Tasksviewmodeltest
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
- Запустите свой код!
Предупреждение
Не делайте следующее, не инициализируйте
tasksViewModel
с его определением:
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
Это приведет к тому, что тот же экземпляр будет использоваться для всех тестов. Это то, чего вы должны избегать, потому что каждый тест должен иметь новый экземпляр тестирования предмета (в этом случае ViewModel).
Ваш окончательный код для TasksViewModelTest
должен выглядеть как код ниже.
Tasksviewmodeltest
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
// Executes each task synchronously using Architecture Components.
@get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
@Test
fun addNewTask_setsNewTaskEvent() {
// When adding a new task
tasksViewModel.addNewTask()
// Then the new task event is triggered
val value = tasksViewModel.newTaskEvent.awaitNextValue()
assertThat(
value?.getContentIfNotHandled(), (not(nullValue()))
)
}
@Test
fun getTasksAddViewVisible() {
// When the filter type is ALL_TASKS
tasksViewModel.setFiltering(TasksFilterType.ALL_TASKS)
// Then the "Add task" action is visible
assertThat(tasksViewModel.tasksAddViewVisible.awaitNextValue(), `is`(true))
}
}
Нажмите здесь, чтобы увидеть различие между начальным кодом, и окончательным кодом.
Чтобы загрузить код для готового CodeLab, вы можете использовать команду GIT ниже:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
В качестве альтернативы вы можете загрузить репозиторий в виде zip -файла, расстегнуть его и открыть в Android Studio.
Этот коделаб покрыл:
- Как запустить тесты от Android Studio.
- Разница между локальными (
test
) и инструментальными тестами (androidTest
). - Как написать локальные модульные тесты, используя JUNIT и HAMCREST .
- Настройка тестов ViewModel с помощью библиотеки тестов Androidx .
Курс Udacity:
Документация для разработчиков Android:
- Руководство по архитектуре приложения
- JUNIT4
- Хамрест
- БИБЛИОТЕКА РОБОЛЕКТРИЧЕСКОГО Тестирования
- Библиотека тестирования Androidx
- Архитектура Androidx Компоненты основной тестовой библиотеки
- исходные наборы
- Тест из командной строки
Видео:
Другой:
Ссылки на другие коделабы в этом курсе см. Веденный Android на целевой странице Kotlin CodeLabs.