Эта кодовая лаборатория является частью курса Advanced Android in Kotlin. Вы получите максимальную отдачу от этого курса, если будете последовательно работать с лабораториями кода, но это не обязательно. Все кодовые лаборатории курса перечислены на целевой странице Advanced Android in Kotlin codelabs .
Введение
Когда вы реализовали первую функцию своего первого приложения, вы, вероятно, запустили код, чтобы убедиться, что он работает должным образом. Вы выполнили тест , хотя и ручной тест . Поскольку вы продолжали добавлять и обновлять функции, вы, вероятно, также продолжали запускать свой код и проверять его работу. Но делать это каждый раз вручную утомительно, подвержено ошибкам и не масштабируется.
Компьютеры отлично подходят для масштабирования и автоматизации! Поэтому разработчики в больших и малых компаниях пишут автоматизированные тесты , которые запускаются программным обеспечением и не требуют от вас ручного управления приложением для проверки работы кода.
В этой серии лабораторных работ вы узнаете, как создать набор тестов (называемый набором тестов) для реального приложения.
Эта первая лабораторная работа охватывает основы тестирования на Android, вы напишете свои первые тесты и узнаете, как тестировать LiveData
и ViewModel
s.
Что вы уже должны знать
Вы должны быть знакомы с:
- Язык программирования Котлин
- Следующие основные библиотеки Android Jetpack:
ViewModel
иLiveData
- Архитектура приложения в соответствии с шаблоном из руководств по архитектуре приложений и лабораторных работ по основам Android.
Что вы узнаете
Вы узнаете о следующих темах:
- Как писать и запускать модульные тесты на Android
- Как использовать разработку через тестирование
- Как выбрать инструментальные тесты и локальные тесты
Вы узнаете о следующих библиотеках и концепциях кода:
- Юнит4
- Хэмкрест
- Библиотека тестов 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 с фрагментами, репозиторий и комнату. Если вы знакомы с любым из приведенных ниже примеров, это приложение имеет аналогичную архитектуру:
- Номер с видом Codelab
- Учебные лаборатории по основам Android Kotlin
- Усовершенствованные учебные коды для Android
- Образец подсолнуха Android
- Учебный курс «Разработка приложений для Android с помощью Kotlin Udacity»
Более важно, чтобы вы понимали общую архитектуру приложения, чем глубоко понимали логику на любом уровне.
Вот краткое описание пакетов, которые вы найдете:
Пакет: | |
| Экран добавления или редактирования задачи: код слоя пользовательского интерфейса для добавления или редактирования задачи. |
| Уровень данных: это относится к уровню данных задач. Он содержит базу данных, сеть и код репозитория. |
| Экран статистики: код слоя пользовательского интерфейса для экрана статистики. |
| Экран сведений о задаче: код слоя пользовательского интерфейса для отдельной задачи. |
| Экран задач: код слоя пользовательского интерфейса для списка всех задач. |
| Вспомогательные классы: общие классы, используемые в различных частях приложения, например, для макета обновления смахивания, используемого на нескольких экранах. |
Слой данных (.data)
Это приложение включает в себя смоделированный сетевой уровень в удаленном пакете и уровень базы данных в локальном пакете. Для простоты в этом проекте сетевой уровень моделируется только с помощью HashMap
с задержкой, а не с реальными сетевыми запросами.
DefaultTasksRepository
координирует или является посредником между сетевым уровнем и уровнем базы данных и возвращает данные на уровень пользовательского интерфейса.
Уровень пользовательского интерфейса (.addedittask, .statistics, .taskdetail, .tasks)
Каждый из пакетов слоя пользовательского интерфейса содержит фрагмент и модель представления, а также любые другие классы, необходимые для пользовательского интерфейса (например, адаптер для списка задач). TaskActivity
— это действие, содержащее все фрагменты.
Навигация
Навигация для приложения управляется компонентом навигации . Он определен в файле 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)
Эти папки известны как исходные наборы . Исходные наборы — это папки, содержащие исходный код вашего приложения. Исходные наборы, окрашенные в зеленый цвет ( androidTest и test ), содержат ваши тесты. Когда вы создаете новый проект Android, по умолчанию вы получаете следующие три исходных набора. Они есть:
-
main
: содержит код вашего приложения. Этот код используется всеми различными версиями приложения, которые вы можете создать (известными как варианты сборки ). -
androidTest
: содержит тесты, известные как инструментальные тесты. -
test
: содержит тесты, известные как локальные тесты.
Разница между локальными тестами и инструментальными тестами заключается в том, как они выполняются.
Локальные тесты ( исходный набор test
)
Эти тесты запускаются локально на JVM вашего компьютера для разработки и не требуют эмулятора или физического устройства. Из-за этого они бегают быстро, но их верность ниже, а это означает, что они действуют не так, как в реальном мире.
В Android Studio локальные тесты представлены значком зеленого и красного треугольника.
Инструментированные тесты ( исходный набор androidTest
)
Эти тесты выполняются на реальных или эмулированных устройствах Android, поэтому они отражают то, что происходит в реальном мире, но при этом они намного медленнее.
В Android Studio инструментальные тесты представлены Android с зеленым и красным треугольным значком.
Шаг 1. Запустите локальный тест
- Открывайте
test
папку, пока не найдете файл ExampleUnitTest.kt . - Щелкните его правой кнопкой мыши и выберите Run ExampleUnitTest .
Вы должны увидеть следующий вывод в окне « Выполнить» в нижней части экрана:
- Обратите внимание на зеленые галочки и разверните результаты теста, чтобы подтвердить, что один тест с именем
addition_isCorrect
пройден. Приятно знать, что дополнение работает так, как ожидалось!
Шаг 2. Сделайте так, чтобы тест провалился
Ниже приведен тест, который вы только что выполнили.
ПримерUnitTest.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
(каждая функция — это отдельный тест). - u8обычно содержат операторы утверждений.
Android использует тестовую библиотеку JUnit для тестирования (в этой кодовой лаборатории JUnit4). И утверждения, и аннотация @Test
исходят из JUnit.
Утверждение — это ядро вашего теста. Это оператор кода, который проверяет, что ваш код или приложение ведут себя так, как ожидалось. В этом случае утверждением является assertEquals(4, 2 + 2)
, которое проверяет, что 4 равно 2 + 2.
Чтобы увидеть, как выглядит неудачный тест, добавьте утверждение, которое, как вы легко видите, должно провалиться. Он проверит, что 3 равно 1+1.
- Добавьте
assertEquals(3, 1 + 1)
к тестуaddition_isCorrect
.
ПримерUnitTest.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
.
ПримерInstrumentedTest
@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
.
СтатистикаUtils.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
и выберите Generate > Test .
Откроется диалоговое окно « Создать тест »:
- Измените имя класса: на
StatisticsUtilsTest
(вместоStatisticsUtilsKtTest
; лучше не использовать KT в имени тестового класса). - Оставьте остальные значения по умолчанию. JUnit 4 — подходящая библиотека для тестирования. Целевой пакет правильный (он отражает расположение класса
StatisticsUtils
), и вам не нужно устанавливать какие-либо флажки (это просто создает дополнительный код, но вы напишете свой тест с нуля). - Нажмите ОК
Откроется диалоговое окно « Выберите каталог назначения »:
Вы будете проводить локальный тест, потому что ваша функция выполняет математические вычисления и не будет включать какой-либо специфичный для Android код. Таким образом, нет необходимости запускать его на реальном или эмулируемом устройстве.
- Выберите
test
каталог (неandroidTest
), потому что вы будете писать локальные тесты. - Нажмите ОК .
- Обратите внимание на сгенерированный класс
StatisticsUtilsTest
вtest/statistics/
.
Шаг 2: Напишите свою первую тестовую функцию
Вы собираетесь написать тест, который проверяет:
- если нет выполненных задач и одна активная задача,
- что процент активных тестов равен 100%,
- а процент выполненных задач равен 0%.
- Откройте
StatisticsUtilsTest
. - Создайте функцию с именем
getActiveAndCompletedStats_noCompleted_returnsHundredZero
.
СтатистикаUtilsTest.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)
Вот полный код.
СтатистикаUtilsTest.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()
, чтобы использовать assertThatassertThat
вместоassertEquals
.
// REPLACE
assertEquals(result.completedTasksPercent, 0f)
assertEquals(result.activeTasksPercent, 100f)
// WITH
assertThat(result.activeTasksPercent, `is`(100f))
assertThat(result.completedTasksPercent, `is`(0f))
Обратите внимание, что вы можете использовать import import org.hamcrest.Matchers.`is`
, если будет предложено.
Окончательный тест будет выглядеть как код ниже.
СтатистикаUtilsTest.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()
)
}
Чтобы исправить код и написать тесты, вы будете использовать разработку через тестирование. Разработка через тестирование следует следующим шагам.
- Напишите тест, используя структуру Given, When, Then и имя, соответствующее соглашению.
- Подтвердите, что тест не пройден.
- Напишите минимальный код, чтобы пройти тест.
- Повторить для всех тестов!
Вместо того, чтобы начинать с исправления ошибки, вы начнете с написания тестов. Затем вы можете подтвердить, что у вас есть тесты, защищающие вас от случайного повторного появления этих ошибок в будущем.
- Если есть пустой список (
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 и сначала написав тесты, вы обеспечили следующее:
- Новая функциональность всегда имеет связанные тесты; таким образом, ваши тесты действуют как документация того, что делает ваш код.
- Ваши тесты проверяют правильные результаты и защищают от ошибок, которые вы уже видели.
Решение: написать больше тестов
Вот все тесты и соответствующий код функции.
СтатистикаUtilsTest.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))
}
}
СтатистикаUtils.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 .
- На экране « Создать тест » нажмите « ОК », чтобы принять его (нет необходимости изменять какие-либо настройки по умолчанию).
- В диалоговом окне Choose Destination Directory выберите тестовый каталог.
Шаг 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 включают классы и методы, которые предоставляют вам версии таких компонентов, как приложения и действия, предназначенные для тестов. Если у вас есть локальный тест , в котором вам нужны смоделированные классы платформы Android (например, контекст приложения), выполните следующие действия, чтобы правильно настроить тест AndroidX.
- Добавьте ядро теста AndroidX и дополнительные зависимости
- Добавьте зависимость библиотеки Robolectric Testing
- Аннотируйте класс с помощью средства запуска тестов AndroidJunit4.
- Написать тестовый код AndroidX
Вы собираетесь выполнить эти шаги, а затем понять, что они делают вместе.
Шаг 3. Добавьте зависимости Gradle
- Скопируйте эти зависимости в файл
build.gradle
вашего модуля приложения, чтобы добавить базовые зависимости AndroidX Test core и ext, а также зависимость тестирования Robolectric.
приложение/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
- Добавьте
@RunWith(AndroidJUnit4::class)
над тестовым классом.
TasksViewModelTest.kt
@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
// Test code
}
Шаг 5. Используйте тест AndroidX
На этом этапе вы можете использовать тестовую библиотеку AndroidX. Сюда входит метод t
ApplicationProvider.getApplicationContex
который получает контекст приложения.
- Создайте
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?
AndroidX Test — это набор библиотек для тестирования. Он включает в себя классы и методы, которые предоставляют версии таких компонентов, как приложения и действия, предназначенные для тестов. Например, этот код, который вы написали, является примером тестовой функции AndroidX для получения контекста приложения.
ApplicationProvider.getApplicationContext()
Одним из преимуществ API-интерфейсов AndroidX Test является то, что они созданы для работы как с локальными, так и с инструментальными тестами. Это приятно, потому что:
- Вы можете запустить тот же тест, что и локальный тест или инструментальный тест.
- Вам не нужно изучать различные API-интерфейсы тестирования для локальных и инструментальных тестов.
Например, поскольку вы написали свой код с использованием библиотек тестирования AndroidX, вы можете переместить класс 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
). Далее вы научитесь писать утверждения с помощью 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 для основной библиотеки тестирования компонентов архитектуры (которая содержит это правило).
приложение/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
}
Это достаточно сложный метод. It creates a Kotlin extension function called getOrAwaitValue
which adds an observer, gets the LiveData
value, and then cleans up the observer—basically a short, reusable version of the observeForever
code shown above. For a full explanation of this class, check out this blog post .
Step 3. Use getOrAwaitValue to write the assertion
In this step, you use the getOrAwaitValue
method and write an assert statement that checks that the newTaskEvent
was triggered.
- Get the
LiveData
value fornewTaskEvent
usinggetOrAwaitValue
.
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
- Assert that the value is not null.
assertThat(value.getContentIfNotHandled(), (not(nullValue())))
The complete test should look like the code below.
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()))
}
}
- Run your code and watch the test pass!
Now that you've seen how to write a test, write one on your own. In this step, using the skills you've learned, practice writing another TasksViewModel
test.
Step 1. Write your own ViewModel test
You'll write setFilterAllTasks_tasksAddViewVisible()
. This test should check that if you've set your filter type to show all tasks, that the Add task button is visible.
- Using
addNewTask_setsNewTaskEvent()
for reference, write a test inTasksViewModelTest
calledsetFilterAllTasks_tasksAddViewVisible()
that sets the filtering mode toALL_TASKS
and asserts that thetasksAddViewVisible
LiveData istrue
.
Use the code below to get started.
TasksViewModelTest
@Test
fun setFilterAllTasks_tasksAddViewVisible() {
// Given a fresh ViewModel
// When the filter type is ALL_TASKS
// Then the "Add task" action is visible
}
Note:
- The
TasksFilterType
enum for all tasks isALL_TASKS.
- The visibility of the button to add a task is controlled by the
LiveData
tasksAddViewVisible.
- Run your test.
Step 2. Compare your test to the solution
Compare your solution to the solution below.
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))
}
Check whether you do the following:
- You create your
tasksViewModel
using the same AndroidXApplicationProvider.getApplicationContext()
statement. - You call the
setFiltering
method, passing in theALL_TASKS
filter type enum. - You check that the
tasksAddViewVisible
is true, using thegetOrAwaitNextValue
method.
Step 3. Add a @Before rule
Notice how at the start of both of your tests, you define a TasksViewModel
.
TasksViewModelTest
// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
When you have repeated setup code for multiple tests, you can use the @Before annotation to create a setup method and remove repeated code. Since all of these tests are going to test the TasksViewModel
, and need a view model, move this code to a @Before
block.
- Create a
lateinit
instance variable calledtasksViewModel|
. - Create a method called
setupViewModel
. - Annotate it with
@Before
. - Move the view model instantiation code to
setupViewModel
.
TasksViewModelTest
// Subject under test
private lateinit var tasksViewModel: TasksViewModel
@Before
fun setupViewModel() {
tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
}
- Run your code!
Warning
Do not do the following, do not initialize the
tasksViewModel
with its definition:
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
This will cause the same instance to be used for all tests. This is something you should avoid because each test should have a fresh instance of the subject under test (the ViewModel in this case).
Your final code for TasksViewModelTest
should look like the code below.
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))
}
}
Click here to see a diff between the code you started and the final code.
To download the code for the finished codelab, you can use the git command below:
$ git clone https://github.com/googlecodelabs/android-testing.git $ cd android-testing $ git checkout end_codelab_1
Alternatively you can download the repository as a Zip file, unzip it, and open it in Android Studio.
This codelab covered:
- How to run tests from Android Studio.
- The difference between local (
test
) and instrumentation tests (androidTest
). - How to write local unit tests using JUnit and Hamcrest .
- Setting up ViewModel tests with the AndroidX Test Library .
Udacity course:
Документация для разработчиков Android:
- Guide to app architecture
- JUnit4
- Hamcrest
- Robolectric Testing library
- AndroidX Test Library
- AndroidX Architecture Components Core Test Library
- source sets
- Test from the command line
Videos:
Other:
For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.