Основы тестирования

Эта практическая работа входит в курс «Advanced Android in Kotlin». Вы получите максимальную пользу от этого курса, выполняя задания последовательно, но это не обязательно. Все практическая работа курса перечислены на целевой странице практической работы «Advanced Android in Kotlin» .

Введение

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

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

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

В этой первой лабораторной работе рассматриваются основы тестирования на Android, вы напишете свои первые тесты и узнаете, как тестировать LiveData и ViewModel .

Что вам уже следует знать

Вам должно быть знакомо:

Чему вы научитесь

Вы узнаете о следующих темах:

  • Как писать и запускать модульные тесты на Android
  • Как использовать разработку через тестирование
  • Как выбрать инструментальные тесты и локальные тесты

Вы узнаете о следующих библиотеках и концепциях кода:

Что ты будешь делать?

  • Настраивайте, запускайте и интерпретируйте локальные и инструментированные тесты в Android.
  • Написание модульных тестов в Android с использованием JUnit4 и Hamcrest.
  • Напишите простые тесты LiveData и ViewModel .

В этой серии практических занятий вы будете работать с приложением TO-DO Notes. Оно позволяет записывать задачи и отображать их в виде списка. Вы можете отмечать их как выполненные или нет, фильтровать или удалять.

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

Чтобы начать, скачайте код:

Загрузить ZIP-архив

В качестве альтернативы вы можете клонировать репозиторий 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. Если вы знакомы с каким-либо из примеров ниже, то это приложение имеет похожую архитектуру:

Важнее понимать общую архитектуру приложения, чем глубоко понимать логику на каком-либо одном уровне.

Вот список пакетов, которые вы найдете:

Пакет: com.example.android.architecture.blueprints.todoapp

.addedittask

Экран добавления или редактирования задачи: код слоя пользовательского интерфейса для добавления или редактирования задачи.

.data

Уровень данных: отвечает за уровень данных задач. Он содержит код базы данных, сети и репозитория.

.statistics

Экран статистики: код слоя пользовательского интерфейса для экрана статистики.

.taskdetail

Экран сведений о задаче: код слоя пользовательского интерфейса для одной задачи.

.tasks

Экран задач: код слоя пользовательского интерфейса для списка всех задач.

.util

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

Уровень данных (.data)

Это приложение включает в себя смоделированный сетевой уровень в удалённом пакете и уровень базы данных в локальном пакете. Для простоты в этом проекте сетевой уровень смоделирован только с помощью HashMap с задержкой, а не с помощью реальных сетевых запросов.

DefaultTasksRepository координирует или выступает посредником между сетевым уровнем и уровнем базы данных и возвращает данные на уровень пользовательского интерфейса.

Уровень пользовательского интерфейса (.addedittask, .statistics, .taskdetail, .tasks)

Каждый пакет слоя пользовательского интерфейса содержит фрагмент и модель представления, а также любые другие классы, необходимые для пользовательского интерфейса (например, адаптер для списка задач). TaskActivity — это активность, содержащая все фрагменты.

Навигация

Навигация в приложении управляется компонентом Navigation . Он определён в файле nav_graph.xml . Навигация активируется в моделях представления с помощью класса Event ; модели представления также определяют, какие аргументы следует передавать. Фрагменты отслеживают Event и осуществляют фактическую навигацию между экранами.

В этом задании вы проведете свои первые тесты.

  1. В 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: Проведите локальный тест

  1. Откройте test папку и найдите файл ExampleUnitTest.kt .
  2. Щелкните по нему правой кнопкой мыши и выберите Запустить ExampleUnitTest .

В окне « Выполнить » в нижней части экрана вы должны увидеть следующий вывод:

  1. Обратите внимание на зелёные галочки и разверните результаты теста, чтобы убедиться, что один тест с именем 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.

  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
   }
}
  1. Проведите тест.
  1. В результатах теста обратите внимание на значок X рядом с тестом.

  1. Также обратите внимание:
  • Одно неверное утверждение делает весь тест недействительным.
  • Вам сообщают ожидаемое значение (3) в сравнении со значением, которое было фактически рассчитано (2).
  • Вы перенаправлены на строку невыполненного утверждения (ExampleUnitTest.kt:16) .

Шаг 3: Проведение инструментального теста

Инструментированные тесты находятся в исходном наборе androidTest .

  1. Откройте исходный набор androidTest .
  2. Запустите тест под названием 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: Создайте тестовый класс

  1. В main исходном наборе, в todoapp.statistics , откройте StatisticsUtils.kt .
  2. Найдите функцию 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 предоставляет вам инструменты для создания тестовых заглушек, которые помогут вам реализовать тесты для этой функции.

  1. Щелкните правой кнопкой мыши getActiveAndCompletedStats и выберите «Создать» > «Тест» .

Откроется диалоговое окно «Создать тест» :

  1. Измените имя класса: на StatisticsUtilsTest (вместо StatisticsUtilsKtTest ; немного лучше не использовать KT в имени тестового класса).
  2. Сохраните остальные значения по умолчанию. JUnit 4 — подходящая библиотека для тестирования. Целевой пакет указан верно (он совпадает с расположением класса StatisticsUtils ), и вам не нужно устанавливать какие-либо флажки (это просто сгенерирует дополнительный код, но тест вы напишете с нуля).
  3. Нажмите ОК

Откроется диалоговое окно «Выбор целевого каталога» :

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

  1. Выберите test каталог (не androidTest ), поскольку вы будете писать локальные тесты.
  2. Нажмите ОК .
  3. Обратите внимание на сгенерированный класс StatisticsUtilsTest в test/statistics/ .

Шаг 2: Напишите свою первую тестовую функцию

Вам предстоит написать тест, который проверит:

  • если нет выполненных задач и есть одна активная задача,
  • что процент активных тестов составляет 100%,
  • а процент выполненных задач — 0%.
  1. Откройте StatisticsUtilsTest .
  2. Создайте функцию с именем getActiveAndCompletedStats_noCompleted_returnsHundredZero .

StatisticsUtilsTest.kt

class StatisticsUtilsTest {

    fun getActiveAndCompletedStats_noCompleted_returnsHundredZero() {
        // Create an active task

        // Call your function

        // Check the result
    }
}
  1. Добавьте аннотацию @Test над именем функции, чтобы указать, что это тест.
  2. Составьте список задач.
// Create an active task 
val tasks = listOf<Task>(
            Task("title", "desc", isCompleted = false)
        )
  1. Вызовите getActiveAndCompletedStats с этими задачами.
// Call your function
val result = getActiveAndCompletedStats(tasks)
  1. Проверьте, соответствует ли 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)
    }
}
  1. Запустите тест (щелкните правой кнопкой мыши StatisticsUtilsTest и выберите Выполнить ).

Должно пройти:

Шаг 3: Добавьте зависимость Hamcrest

Поскольку ваши тесты служат документацией того, что делает ваш код, приятно, когда они понятны человеку. Сравните следующие два утверждения:

assertEquals(result.completedTasksPercent, 0f)

// versus

assertThat(result.completedTasksPercent, `is`(0f))

Второе утверждение гораздо более похоже на человеческое предложение. Оно написано с использованием фреймворка для суждений Hamcrest . Ещё один хороший инструмент для написания читаемых утверждений — библиотека Truth . В этой лабораторной работе вы будете использовать Hamcrest для написания утверждений.

  1. Откройте 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 для написания утверждений

  1. Обновите тест 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))

    }
}
  1. Запустите обновленный тест, чтобы убедиться, что он все еще работает!

Эта лабораторная работа не научит вас всем тонкостям Hamcrest, поэтому, если вы хотите узнать больше, ознакомьтесь с официальным руководством .

Это факультативное задание для практики.

В этом задании вы напишете ещё больше тестов, используя JUnit и Hamcrest. Вы также будете писать тесты, используя стратегию, основанную на методологии разработки через тестирование (Test Driven Development) . Разработка через тестирование (TDD) — это школа программирования, которая гласит, что вместо написания кода функций сначала нужно написать тесты. Затем вы пишете код функций, стремясь к их прохождению.

Шаг 1. Напишите тесты

Напишите тесты для случая, когда у вас есть обычный список задач:

  1. Если есть одна завершенная задача и нет активных задач, процент activeTasks должен быть 0f , а процент завершенных задач должен быть 100f .
  2. Если имеется две завершенные задачи и три активные задачи, процент завершения должен быть 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()
   )
  
}

Чтобы исправить код и написать тесты, вы будете использовать разработку через тестирование. Разработка через тестирование включает в себя следующие этапы.

  1. Напишите тест, используя структуру «Дано, Когда, Тогда» и дав имя, соответствующее соглашению.
  2. Подтвердите, что тест не пройден.
  3. Напишите минимальный код, чтобы тест прошёл.
  4. Повторите для всех тестов!

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

  1. Если список пустой ( emptyList() ), то оба процента должны быть равны 0f.
  2. Если при загрузке задач произошла ошибка, список будет null , а оба процента должны быть равны 0f.
  3. Запустите тесты и убедитесь, что они не пройдены :

Шаг 3. Исправьте ошибку

Теперь, когда у вас есть тесты, исправьте ошибку.

  1. Исправьте ошибку в 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
        )
    }
}
  1. Повторите тесты и убедитесь, что теперь все тесты пройдены!

Следуя принципам 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 .

  1. Откройте класс, который вы хотите протестировать, в пакете tasks TasksViewModel.
  2. В коде щелкните правой кнопкой мыши по имени класса TasksViewModel -> Generate -> Test .

  1. На экране «Создать тест» нажмите кнопку «ОК» , чтобы принять изменения (не нужно менять какие-либо настройки по умолчанию).
  2. В диалоговом окне «Выбор целевого каталога» выберите тестовый каталог.

Шаг 2. Начните писать тест ViewModel

На этом этапе вы добавляете тест модели представления, чтобы проверить, что при вызове метода addNewTask запускается Event открытия нового окна задачи.

  1. Создайте новый тест с названием 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:

  1. Добавьте ядро AndroidX Test и зависимости ext
  2. Добавьте зависимость библиотеки Robolectric Testing
  3. Аннотируйте класс с помощью тест-раннера AndroidJunit4
  4. Напишите тестовый код AndroidX

Вам предстоит выполнить эти шаги, а затем понять, что они делают вместе.

Шаг 3. Добавьте зависимости Gradle

  1. Скопируйте эти зависимости в файл 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

  1. Добавьте @RunWith(AndroidJUnit4::class) над вашим тестовым классом.

TasksViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {
    // Test code
}

Шаг 5. Используйте AndroidX Test

На этом этапе вы можете использовать библиотеку AndroidX Test. Она включает метод ApplicationProvider.getApplicationContex t , который получает контекст приложения.

  1. Создайте TasksViewModel с помощью ApplicationProvider.getApplicationContext() из тестовой библиотеки AndroidX.

TasksViewModelTest.kt

// Given a fresh ViewModel
val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
  1. Вызовите 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
    }
  1. Запустите тест, чтобы убедиться, что он работает.

Концепция: Как работает 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.

  1. Добавьте следующую строку в файл 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 рекомендуется сделать две вещи:

  1. Использовать InstantTaskExecutorRule
  2. Обеспечить наблюдение LiveData

Шаг 1. Используйте InstantTaskExecutorRule

InstantTaskExecutorRule — это правило JUnit . При использовании его с аннотацией @get:Rule определённый код в классе InstantTaskExecutorRule запускается до и после тестов (чтобы увидеть точный код, воспользуйтесь сочетанием клавиш Command+B для просмотра файла).

Это правило запускает все фоновые задания, связанные с компонентами архитектуры, в одном потоке, чтобы результаты теста появлялись синхронно и в повторяющемся порядке. Используйте это правило при написании тестов, включающих тестирование LiveData!

  1. Добавьте зависимость gradle для базовой тестовой библиотеки Architecture Components (которая содержит это правило).

приложение/build.gradle

testImplementation "androidx.arch.core:core-testing:$archTestingVersion"
  1. Открыть TasksViewModelTest.kt
  2. Добавьте 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 , чтобы...

Чтобы получить ожидаемое поведение 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 , чтобы упростить добавление наблюдателей.

  1. Создайте новый файл Kotlin с именем LiveDataTestUtil.kt в вашем исходном наборе test .


  1. Скопируйте и вставьте код ниже.

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 .

  1. Получите значение LiveData для newTaskEvent с помощью getOrAwaitValue .
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. Подтвердите, что значение не равно нулю.
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()))


    }

}
  1. Запустите свой код и посмотрите тестовый проход!

Теперь, когда вы видели, как написать тест, напишите его самостоятельно. На этом этапе, используя навыки, которые вы изучили, практикуйте написание другого теста TasksViewModel .

Шаг 1. Напишите свой собственный тест ViewModel

Вы напишете setFilterAllTasks_tasksAddViewVisible() . Этот тест должен проверить, что если вы установили тип фильтра, чтобы показать все задачи, то кнопка «Добавить задачу» видна.

  1. Используя 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.
  1. Запустите свой тест.

Шаг 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 , используя тот же оператор Androidx ApplicationProvider.getApplicationContext() .
  • Вы называете метод setFiltering , проходя в перечислении типа фильтра ALL_TASKS .
  • Вы проверяете, что tasksAddViewVisible верна, используя метод getOrAwaitNextValue .

Шаг 3. Добавить правило @before

Обратите внимание, как в начале обоих ваших тестов вы определяете TasksViewModel .

Tasksviewmodeltest

        // Given a fresh ViewModel
        val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())

Когда у вас есть код настройки для нескольких тестов, вы можете использовать аннотацию @before для создания метода настройки и удалить повторный код. Поскольку все эти тесты будут протестировать TasksViewModel и понадобится модель представления, перенесите этот код в блок @Before .

  1. Создайте переменную экземпляра lateinit экземпляра с именем tasksViewModel| Полем
  2. Создайте метод с именем setupViewModel .
  3. Аннотируйте это с @Before .
  4. Переместите код создания модели модели в setupViewModel .

Tasksviewmodeltest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. Запустите свой код!

Предупреждение

Не делайте следующее, не инициализируйте

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.

Загрузить ZIP-архив

Этот коделаб покрыл:

  • Как запустить тесты от Android Studio.
  • Разница между локальными ( test ) и инструментальными тестами ( androidTest ).
  • Как написать локальные модульные тесты, используя JUNIT и HAMCREST .
  • Настройка тестов ViewModel с помощью библиотеки тестов Androidx .

Курс Udacity:

Документация для разработчиков Android:

Видео:

Другой:

Ссылки на другие коделабы в этом курсе см. Веденный Android на целевой странице Kotlin CodeLabs.