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

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

Введение

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

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

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

Эта первая лабораторная работа охватывает основы тестирования на Android, вы напишете свои первые тесты и узнаете, как тестировать LiveData и ViewModel s.

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

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

Что вы узнаете

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

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

Более важно, чтобы вы понимали общую архитектуру приложения, чем глубоко понимали логику на любом уровне.

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

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

.addedittask

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

.data

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

.statistics

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

.taskdetail

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

.tasks

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

.util

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

Слой данных (.data)

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

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

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

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

Навигация

Навигация для приложения управляется компонентом навигации . Он определен в файле 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)

Эти папки известны как исходные наборы . Исходные наборы — это папки, содержащие исходный код вашего приложения. Исходные наборы, окрашенные в зеленый цвет ( androidTest и test ), содержат ваши тесты. Когда вы создаете новый проект Android, по умолчанию вы получаете следующие три исходных набора. Они есть:

  • main : содержит код вашего приложения. Этот код используется всеми различными версиями приложения, которые вы можете создать (известными как варианты сборки ).
  • androidTest : содержит тесты, известные как инструментальные тесты.
  • test : содержит тесты, известные как локальные тесты.

Разница между локальными тестами и инструментальными тестами заключается в том, как они выполняются.

Локальные тесты ( исходный набор test )

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

В Android Studio локальные тесты представлены значком зеленого и красного треугольника.

Инструментированные тесты ( исходный набор androidTest )

Эти тесты выполняются на реальных или эмулированных устройствах Android, поэтому они отражают то, что происходит в реальном мире, но при этом они намного медленнее.

В Android Studio инструментальные тесты представлены Android с зеленым и красным треугольным значком.

Шаг 1. Запустите локальный тест

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

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

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

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

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

Шаг 3. Запустите инструментальный тест

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

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

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

  1. Щелкните правой кнопкой мыши getActiveAndCompletedStats и выберите Generate > Test .

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

  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 .

СтатистикаUtilsTest.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)

Вот полный код.

Статистика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)
    }
}
  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 assertThat вместо 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))

    }
}
  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. Напишите тест, используя структуру Given, When, Then и имя, соответствующее соглашению.
  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 и сначала написав тесты, вы обеспечили следующее:

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

Решение: написать больше тестов

Вот все тесты и соответствующий код функции.

Статистика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 .

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

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

Шаг 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 включают классы и методы, которые предоставляют вам версии таких компонентов, как приложения и действия, предназначенные для тестов. Если у вас есть локальный тест , в котором вам нужны смоделированные классы платформы Android (например, контекст приложения), выполните следующие действия, чтобы правильно настроить тест AndroidX.

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

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

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

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

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

TasksViewModelTest.kt

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

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

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

  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?

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.

  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 ). Далее вы научитесь писать утверждения с помощью 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 для основной библиотеки тестирования компонентов архитектуры (которая содержит это правило).

приложение/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
}

Это достаточно сложный метод. 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.

  1. Get the LiveData value for newTaskEvent using getOrAwaitValue .
val value = tasksViewModel.newTaskEvent.getOrAwaitValue()
  1. 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()))


    }

}
  1. 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.

  1. Using addNewTask_setsNewTaskEvent() for reference, write a test in TasksViewModelTest called setFilterAllTasks_tasksAddViewVisible() that sets the filtering mode to ALL_TASKS and asserts that the tasksAddViewVisible LiveData is true .


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 is ALL_TASKS.
  • The visibility of the button to add a task is controlled by the LiveData tasksAddViewVisible.
  1. 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 AndroidX ApplicationProvider.getApplicationContext() statement.
  • You call the setFiltering method, passing in the ALL_TASKS filter type enum.
  • You check that the tasksAddViewVisible is true, using the getOrAwaitNextValue 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.

  1. Create a lateinit instance variable called tasksViewModel| .
  2. Create a method called setupViewModel .
  3. Annotate it with @Before .
  4. Move the view model instantiation code to setupViewModel .

TasksViewModelTest

    // Subject under test
    private lateinit var tasksViewModel: TasksViewModel

    @Before
    fun setupViewModel() {
        tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext())
    }
  1. 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.

Download Zip

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:

Videos:

Other:

For links to other codelabs in this course, see the Advanced Android in Kotlin codelabs landing page.