Введение в тестовые двойники и внедрение зависимостей

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

Введение

Эта вторая лабораторная работа по тестированию посвящена тестовым двойникам: когда их использовать в Android и как их реализовать с помощью внедрения зависимостей, шаблона Service Locator и библиотек. В ходе работы вы научитесь писать:

  • Модульные тесты репозитория
  • Фрагменты и тесты интеграции viewmodel
  • Фрагментные навигационные тесты

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

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

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

  • Как спланировать стратегию тестирования
  • Как создавать и использовать тестовые двойники, а именно подделки и фиктивные тесты
  • Как использовать ручное внедрение зависимостей в Android для модульных и интеграционных тестов
  • Как применять шаблон локатора услуг
  • Как тестировать репозитории, фрагменты, модели представлений и компонент навигации

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

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

  • Напишите модульные тесты для репозитория, используя тестовый двойник и внедрение зависимостей.
  • Напишите модульные тесты для модели представления, используя тестовый двойник и внедрение зависимостей.
  • Напишите интеграционные тесты для фрагментов и их моделей представлений, используя фреймворк тестирования пользовательского интерфейса Espresso.
  • Напишите навигационные тесты с использованием Mockito и Espresso.

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

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

Загрузить код

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

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

В качестве альтернативы вы можете клонировать репозиторий Github для кода:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_1

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

Шаг 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 и осуществляют фактическую навигацию между экранами.

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

В этом разделе рассматриваются некоторые передовые практики тестирования в целом, применимые к Android.

Пирамида тестирования

При обдумывании стратегии тестирования следует учитывать три взаимосвязанных аспекта тестирования:

  • Область действия — какую часть кода затрагивает тест? Тесты могут выполняться для одного метода, для всего приложения или где-то посередине.
  • Скорость — Насколько быстро выполняется тест? Скорость выполнения теста может варьироваться от миллисекунд до нескольких минут.
  • Точность — насколько тест соответствует реальному миру? Например, если часть тестируемого кода должна выполнить сетевой запрос, действительно ли тестовый код выполняет этот запрос или имитирует результат? Если тест действительно взаимодействует с сетью, это означает более высокую точность. Однако это может привести к увеличению времени выполнения теста, ошибкам в случае сбоя сети или к его высокой стоимости.

Между этими аспектами существуют неизбежные компромиссы. Например, скорость и точность — это компромисс: чем быстрее тест, тем ниже его точность, и наоборот. Один из распространённых способов разделить автоматизированные тесты — это выделить три категории:

  • Модульные тесты — это узкоспециализированные тесты, которые выполняются для одного класса, обычно для одного метода в этом классе. Если модульный тест завершается неудачей, вы можете точно определить, где в коде возникла проблема. Они имеют низкую точность, поскольку в реальном мире ваше приложение включает в себя гораздо больше, чем выполнение одного метода или класса. Они достаточно быстры, чтобы запускаться при каждом изменении кода. Чаще всего это локально выполняемые тесты (в исходном наборе test ). Пример: тестирование отдельных методов в моделях представлений и репозиториях.
  • Интеграционные тесты — они проверяют взаимодействие нескольких классов, чтобы убедиться в их ожидаемом поведении при совместном использовании. Один из способов структурировать интеграционные тесты — это тестирование одной функции, например, возможности сохранения задачи. Они тестируют больший объём кода, чем модульные тесты, но при этом оптимизированы для быстрого выполнения, а не для обеспечения полной точности. В зависимости от ситуации, их можно запускать как локально, так и в качестве инструментальных тестов. Пример: тестирование всей функциональности одной пары фрагмента и модели представления.
  • Сквозное тестирование (E2E) — тестирование комбинации функций, работающих совместно. Оно тестирует большие фрагменты приложения, максимально точно имитирует реальное использование и поэтому обычно выполняется медленно. Оно обеспечивает максимальную точность и подтверждает, что ваше приложение действительно работает как единое целое. В целом, эти тесты будут инструментированными (в исходном наборе androidTest ).
    Пример: запуск всего приложения и одновременное тестирование нескольких функций.

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

Архитектура и тестирование

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

Более эффективным подходом было бы разбить логику приложения на несколько методов и классов, что позволило бы тестировать каждый элемент изолированно. Архитектура — это способ разделения и организации кода, что упрощает модульное и интеграционное тестирование. Тестируемое приложение TO-DO имеет определённую архитектуру:



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

  1. Сначала вы выполните модульное тестирование репозитория .
  2. Затем вы будете использовать тестовый двойник в модели представления, который необходим для модульного тестирования и интеграционного тестирования модели представления.
  3. Далее вы научитесь писать интеграционные тесты для фрагментов и их моделей представлений .
  4. Наконец, вы научитесь писать интеграционные тесты , включающие компонент навигации .

Сквозное тестирование будет рассмотрено в следующем уроке.

Когда вы пишете модульный тест для части класса (метода или небольшого набора методов), ваша цель — протестировать только код в этом классе .

Тестирование только кода в определённом классе или классах может быть сложной задачей. Рассмотрим пример. Откройте класс data.source.DefaultTaskRepository в main исходном наборе. Это репозиторий приложения, и именно для него вы будете писать модульные тесты.

Ваша цель — протестировать только код в этом классе. Однако для своей работы DefaultTaskRepository зависит от других классов, таких как LocalTaskDataSource и RemoteTaskDataSource . Другими словами, LocalTaskDataSource и RemoteTaskDataSource являются зависимостями DefaultTaskRepository .

Таким образом, каждый метод в DefaultTaskRepository вызывает методы классов источников данных, которые, в свою очередь, вызывают методы в других классах для сохранения информации в базе данных или связи с сетью.



Например, взгляните на этот метод в DefaultTasksRepo .

    suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>> {
        if (forceUpdate) {
            try {
                updateTasksFromRemoteDataSource()
            } catch (ex: Exception) {
                return Result.Error(ex)
            }
        }
        return tasksLocalDataSource.getTasks()
    }

getTasks — один из самых «простых» вызовов, которые вы можете сделать к своему репозиторию. Этот метод включает чтение из базы данных SQLite и выполнение сетевых вызовов (вызов updateTasksFromRemoteDataSource ). Это требует гораздо больше кода, чем просто код репозитория.

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

  • Вам нужно продумать создание и управление базой данных для проведения даже самых простых тестов в этом репозитории. Это поднимает вопросы: «Должен ли это быть локальный или инструментированный тест?» и стоит ли использовать AndroidX Test для моделирования среды Android.
  • Некоторые части кода, например сетевой код, могут выполняться долго, а иногда даже давать сбои, что приводит к длительным и нестабильным тестам.
  • Ваши тесты могут утратить способность определять, какой код стал причиной сбоя. Они могут начать тестировать код, не относящийся к репозиторию, поэтому, например, ваши так называемые «репозиторские» модульные тесты могут завершиться сбоем из-за проблемы в каком-то зависимом коде, например, в коде базы данных.

Тестовые дубли

Решение этой проблемы заключается в том, что при тестировании репозитория не следует использовать реальный код сети или базы данных , а вместо этого использовать тестовый дублер. Тестовый дублер — это версия класса, созданная специально для тестирования. Он предназначен для замены реальной версии класса в тестах. Это похоже на то, как дублер — это актёр, специализирующийся на трюках и заменяющий настоящего актёра при выполнении опасных действий.

Вот некоторые типы тестовых дублей:

Фальшивый

Тестовый двойник, имеющий «рабочую» реализацию класса, но реализованную таким образом, что она подходит для тестов, но не подходит для производства.

Насмехаться

Тестовый двойник, отслеживающий, какие из его методов были вызваны. Он проходит или не проходит тест в зависимости от того, были ли его методы вызваны корректно.

Заглушка

Тестовый двойник, не содержащий никакой логики и возвращающий только то, что вы запрограммировали. Например, StubTaskRepository можно запрограммировать на возврат определённых комбинаций задач из getTasks .

Дурачок

Тестовый двойник, который передаётся, но не используется, например, если вам нужно просто передать его в качестве параметра. Если бы у вас был NoOpTaskRepository , он бы просто реализовал TaskRepository без кода в каких-либо методах.

Шпион

Тестовый двойник, который также отслеживает некоторую дополнительную информацию; например, если вы создали SpyTaskRepository , он может отслеживать количество вызовов метода addTask .

Более подробную информацию о тест-двойниках можно найти в статье Тестирование в туалете: узнайте о своих тест-двойниках .

Наиболее распространенными тестовыми двойниками, используемыми в Android, являются Fakes и Mocks .

В этой задаче вы создадите тестовый двойник FakeDataSource для модульного теста DefaultTasksRepository , отделенный от реальных источников данных.

Шаг 1: Создайте класс FakeDataSource

На этом этапе вы создадите класс FakeDataSouce , который будет тестовым двойником LocalDataSource и RemoteDataSource .

  1. В наборе исходных текстов теста щелкните правой кнопкой мыши и выберите Создать -> Пакет .

  1. Создайте пакет данных с исходным пакетом внутри.
  2. Создайте новый класс с именем FakeDataSource в пакете data/source .

Шаг 2: Реализация интерфейса TasksDataSource

Чтобы использовать ваш новый класс FakeDataSource в качестве тестового дублера, он должен иметь возможность заменять другие источники данных. Это TasksLocalDataSource и TasksRemoteDataSource .

  1. Обратите внимание, как оба они реализуют интерфейс TasksDataSource .
class TasksLocalDataSource internal constructor(
    private val tasksDao: TasksDao,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : TasksDataSource { ... }

object TasksRemoteDataSource : TasksDataSource { ... }
  1. Заставьте FakeDataSource реализовать TasksDataSource :
class FakeDataSource : TasksDataSource {

}

Android Studio выдаст сообщение о том, что вы не реализовали требуемые методы для TasksDataSource .

  1. Используйте меню быстрого исправления и выберите пункт Реализовать элементы .


  1. Выберите все методы и нажмите ОК .

Шаг 3: Реализуйте метод getTasks в FakeDataSource

FakeDataSource — это особый тип тестового двойника, называемый поддельным . Поддельный — это тестовый двойник, имеющий «рабочую» реализацию класса, но реализованный таким образом, что подходит для тестирования, но не подходит для использования в рабочей среде. «Рабочая» реализация означает, что класс будет выдавать реалистичные выходные данные при заданных входных данных.

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

FakeDataSource

  • позволяет тестировать код в DefaultTasksRepository без необходимости использования реальной базы данных или сети.
  • обеспечивает «достаточно реальную» реализацию для тестов.
  1. Измените конструктор FakeDataSource , чтобы создать var с именем tasks , которая представляет собой MutableList<Task>? со значением по умолчанию — пустым изменяемым списком.
class FakeDataSource(var tasks: MutableList<Task>? = mutableListOf()) : TasksDataSource { // Rest of class }


Это список задач, которые «имитируют» ответ базы данных или сервера. Сейчас наша цель — протестировать метод getTasks репозитория . Он вызывает методы getTasks , deleteAllTasks и saveTask источника данных .

Напишите поддельную версию этих методов:

  1. Напишите getTasks : Если tasks не равно null , верните результат Success . Если tasks равно null , верните результат Error .
  2. Введите deleteAllTasks : очистить список изменяемых задач.
  3. Напишите saveTask : добавьте задачу в список.

Эти методы, реализованные для FakeDataSource , выглядят как код ниже.

override suspend fun getTasks(): Result<List<Task>> {
    tasks?.let { return Success(ArrayList(it)) }
    return Error(
        Exception("Tasks not found")
    )
}


override suspend fun deleteAllTasks() {
    tasks?.clear()
}

override suspend fun saveTask(task: Task) {
    tasks?.add(task)
}

Вот операторы импорта, если необходимо:

import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task

Это похоже на то, как работают реальные локальные и удаленные источники данных.

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

Основная проблема в том, что у вас есть FakeDataSource , но непонятно, как вы его используете в тестах. Он должен заменить TasksRemoteDataSource и TasksLocalDataSource , но только в тестах. И TasksRemoteDataSource , и TasksLocalDataSource являются зависимостями от DefaultTasksRepository , то есть DefaultTasksRepositories требует или «зависит» от этих классов для своего запуска.

В настоящее время зависимости создаются внутри метода init объекта DefaultTasksRepository .

DefaultTasksRepository.kt

class DefaultTasksRepository private constructor(application: Application) {

    private val tasksRemoteDataSource: TasksDataSource
    private val tasksLocalDataSource: TasksDataSource

   // Some other code

    init {
        val database = Room.databaseBuilder(application.applicationContext,
            ToDoDatabase::class.java, "Tasks.db")
            .build()

        tasksRemoteDataSource = TasksRemoteDataSource
        tasksLocalDataSource = TasksLocalDataSource(database.taskDao())
    }
    // Rest of class
}

Поскольку вы создаёте и назначаете taskLocalDataSource и tasksRemoteDataSource внутри DefaultTasksRepository , они, по сути, жёстко прописаны в коде. Подменить их в тестовом двойнике невозможно.

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

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

Без инъекций

Инъекция

Шаг 1: использование внедрения зависимости конструктора в DefaultTasksRepository

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

DefaultTasksRepository.kt

// REPLACE
class DefaultTasksRepository private constructor(application: Application) { // Rest of class }

// WITH

class DefaultTasksRepository(
    private val tasksRemoteDataSource: TasksDataSource,
    private val tasksLocalDataSource: TasksDataSource,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { // Rest of class }
  1. Поскольку вы передали зависимости, удалите метод init . Вам больше не нужно создавать зависимости.
  2. Также удалите старые переменные экземпляра. Вы определяете их в конструкторе:

DefaultTasksRepository.kt

// Delete these old variables
private val tasksRemoteDataSource: TasksDataSource
private val tasksLocalDataSource: TasksDataSource
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
  1. Наконец, обновите метод getRepository , чтобы использовать новый конструктор:

DefaultTasksRepository.kt

    companion object {
        @Volatile
        private var INSTANCE: DefaultTasksRepository? = null

        fun getRepository(app: Application): DefaultTasksRepository {
            return INSTANCE ?: synchronized(this) {
                val database = Room.databaseBuilder(app,
                    ToDoDatabase::class.java, "Tasks.db")
                    .build()
                DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                    INSTANCE = it
                }
            }
        }
    }

Теперь вы используете внедрение зависимости конструктора!

Шаг 2: Используйте FakeDataSource в своих тестах

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

  1. Щелкните правой кнопкой мыши имя класса DefaultTasksRepository и выберите «Сгенерировать» , затем «Тестировать».
  2. Следуйте инструкциям по созданию DefaultTasksRepositoryTest в исходном наборе тестов .
  3. В верхней части нового класса DefaultTasksRepositoryTest добавьте указанные ниже переменные-члены для представления данных в ваших поддельных источниках данных.

DefaultTasksRepositoryTest.kt

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }
  1. Создайте три переменные: две переменные-члена FakeDataSource (по одной для каждого источника данных для вашего репозитория) и переменную для DefaultTasksRepository , которую вы будете тестировать.

DefaultTasksRepositoryTest.kt

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

Создайте метод для настройки и инициализации тестируемого DefaultTasksRepository . Этот DefaultTasksRepository будет использовать ваш тестовый двойник FakeDataSource .

  1. Создайте метод с именем createRepository и аннотируйте его @Before .
  2. Создайте поддельные источники данных, используя списки remoteTasks и localTasks .
  3. Создайте экземпляр tasksRepository , используя два фиктивных источника данных, которые вы только что создали, и Dispatchers.Unconfined .

Окончательный метод должен выглядеть так, как показано ниже.

DefaultTasksRepositoryTest.kt

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

Шаг 3: Напишите тест DefaultTasksRepository getTasks()

Пришло время написать тест DefaultTasksRepository !

  1. Напишите тест для метода getTasks репозитория. Убедитесь, что при вызове getTasks со true (что означает необходимость перезагрузки из удалённого источника данных) данные возвращаются из удалённого источника (а не из локального).

DefaultTasksRepositoryTest.kt

@Test
    fun getTasks_requestsAllTasksFromRemoteDataSource(){
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

При вызове getTasks вы получите ошибку getTasks:

Шаг 4: Добавьте runBlockingTest

Ошибка сопрограммы ожидаема, поскольку getTasks — это функция suspend , и для её вызова необходимо запустить сопрограмму. Для этого требуется область действия сопрограммы. Чтобы устранить эту ошибку, необходимо добавить зависимости Gradle для обработки запуска сопрограмм в тестах.

  1. Добавьте необходимые зависимости для тестирования сопрограмм в исходный набор тестов с помощью testImplementation .

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

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

Не забудьте синхронизироваться!

kotlinx-coroutines-test — это библиотека для тестирования сопрограмм, специально предназначенная для этого. Для запуска тестов используйте функцию runBlockingTest . Эта функция предоставляется библиотекой для тестирования сопрограмм. Она принимает блок кода и запускает его в специальном контексте сопрограммы, который выполняется синхронно и немедленно, то есть действия будут выполняться в определённом порядке. По сути, это позволяет вашим сопрограммам работать как обычным сопрограммам, поэтому она предназначена для тестирования кода.

Используйте runBlockingTest в тестовых классах при вызове функции suspend . Подробнее о работе runBlockingTest и тестировании сопрограмм вы узнаете в следующей лабораторной работе этой серии.

  1. Добавьте @ExperimentalCoroutinesApi над классом. Это означает, что вы знаете, что используете экспериментальный API корутины ( runBlockingTest ) в классе. Без этого вы получите предупреждение.
  2. Вернитесь в DefaultTasksRepositoryTest и добавьте runBlockingTest , чтобы он принял весь ваш тест как «блок» кода.

Этот финальный тест выглядит как код ниже.

DefaultTasksRepositoryTest.kt

import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.hamcrest.core.IsEqual
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test


@ExperimentalCoroutinesApi
class DefaultTasksRepositoryTest {

    private val task1 = Task("Title1", "Description1")
    private val task2 = Task("Title2", "Description2")
    private val task3 = Task("Title3", "Description3")
    private val remoteTasks = listOf(task1, task2).sortedBy { it.id }
    private val localTasks = listOf(task3).sortedBy { it.id }
    private val newTasks = listOf(task3).sortedBy { it.id }

    private lateinit var tasksRemoteDataSource: FakeDataSource
    private lateinit var tasksLocalDataSource: FakeDataSource

    // Class under test
    private lateinit var tasksRepository: DefaultTasksRepository

    @Before
    fun createRepository() {
        tasksRemoteDataSource = FakeDataSource(remoteTasks.toMutableList())
        tasksLocalDataSource = FakeDataSource(localTasks.toMutableList())
        // Get a reference to the class under test
        tasksRepository = DefaultTasksRepository(
            // TODO Dispatchers.Unconfined should be replaced with Dispatchers.Main
            //  this requires understanding more about coroutines + testing
            //  so we will keep this as Unconfined for now.
            tasksRemoteDataSource, tasksLocalDataSource, Dispatchers.Unconfined
        )
    }

    @Test
    fun getTasks_requestsAllTasksFromRemoteDataSource() = runBlockingTest {
        // When tasks are requested from the tasks repository
        val tasks = tasksRepository.getTasks(true) as Success

        // Then tasks are loaded from the remote data source
        assertThat(tasks.data, IsEqual(remoteTasks))
    }

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

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

Модульные тесты должны тестировать только тот класс или метод, который вас интересует. Это называется изолированным тестированием, когда вы четко изолируете свой «модуль» и тестируете только тот код, который является частью этого модуля.

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

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

Шаг 1. Создайте интерфейс TasksRepository

Первым шагом к использованию внедрения зависимости конструктора является создание общего интерфейса, общего для поддельного и реального классов.

Как это выглядит на практике? Взгляните на TasksRemoteDataSource , TasksLocalDataSource и FakeDataSource и обратите внимание, что все они используют один и тот же интерфейс: TasksDataSource . Это позволяет указать в конструкторе DefaultTasksRepository , что вы принимаете TasksDataSource .

DefaultTasksRepository.kt

class DefaultTasksRepository(
   private val tasksRemoteDataSource: TasksDataSource,
   private val tasksLocalDataSource: TasksDataSource,
   private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) {

Это то, что позволяет нам подменять ваш FakeDataSource !

Затем создайте интерфейс для DefaultTasksRepository , как и для источников данных. Он должен включать все публичные методы (публичную API-поверхность) DefaultTasksRepository .

  1. Откройте DefaultTasksRepository и щёлкните правой кнопкой мыши по имени класса. Затем выберите Refactor -> Extract -> Interface .

  1. Выберите Извлечь, чтобы отделить файл.

  1. В окне «Извлечение интерфейса» измените имя интерфейса на TasksRepository .
  2. В разделе «Элементы интерфейса формы» отметьте все элементы, кроме двух сопутствующих элементов и закрытых методов.


  1. Нажмите кнопку «Рефакторинг» . Новый интерфейс TasksRepository должен появиться в пакете data/source .

И DefaultTasksRepository теперь реализует TasksRepository .

  1. Запустите свое приложение (не тесты), чтобы убедиться, что все работает.

Шаг 2. Создайте FakeTestRepository

Теперь, когда у вас есть интерфейс, вы можете создать тестовый двойник DefaultTaskRepository .

  1. В наборе исходных текстов в data/source создайте файл Kotlin и класс FakeTestRepository.kt , а также расширьте интерфейс TasksRepository .

FakeTestRepository.kt

class FakeTestRepository : TasksRepository  {
}

Вам сообщат, что вам необходимо реализовать методы интерфейса.

  1. Наведите указатель мыши на ошибку, пока не появится меню предложений, затем нажмите и выберите Реализовать участников .
  1. Выберите все методы и нажмите ОК .

Шаг 3. Реализуйте методы FakeTestRepository

Теперь у вас есть класс FakeTestRepository с нереализованными методами. Подобно тому, как вы реализовали FakeDataSource , FakeTestRepository будет поддерживаться структурой данных, а не сложным посредничеством между локальными и удалёнными источниками данных.

Обратите внимание, что ваш FakeTestRepository не обязан использовать FakeDataSource или что-то подобное; ему достаточно возвращать реалистичные поддельные выходные данные на основе входных данных. Для хранения списка задач будет использоваться LinkedHashMap , а для наблюдаемых задач MutableLiveData .

  1. В FakeTestRepository добавьте переменную LinkedHashMap , представляющую текущий список задач, и MutableLiveData для наблюдаемых задач.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()


    // Rest of class
}

Реализуйте следующие методы:

  1. getTasks — этот метод должен принимать tasksServiceData и преобразовывать его в список с помощью tasksServiceData.values.toList() , а затем возвращать его как результат Success .
  2. refreshTasks — обновляет значение observableTasks до значения, возвращаемого getTasks() .
  3. observeTasks — создает сопрограмму с использованием runBlocking и запускает refreshTasks , затем возвращает observableTasks .

Ниже приведен код этих методов.

FakeTestRepository.kt

class FakeTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        return Result.Success(tasksServiceData.values.toList())
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    // Rest of class

}

Шаг 4. Добавьте метод для тестирования в addTasks.

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

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

FakeTestRepository.kt

    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }

На этом этапе у вас есть фиктивный репозиторий для тестирования с реализацией нескольких ключевых методов. Теперь используйте его в своих тестах!

В этой задаче вы используете фиктивный класс внутри ViewModel . Используйте внедрение зависимости через конструктор, чтобы подключить два источника данных, добавив переменную TasksRepository в конструктор TasksViewModel .

Этот процесс немного отличается от моделей представлений, поскольку вы не создаёте их напрямую. Например:

class TasksFragment : Fragment() {

    private val viewModel by viewModels<TasksViewModel>()
    
    // Rest of class...

}


Как и в приведённом выше коде, вы используете делегат свойства viewModel's , который создаёт модель представления. Чтобы изменить способ построения модели представления, вам нужно добавить и использовать ViewModelProvider.Factory . Если вы не знакомы с ViewModelProvider.Factory , узнать больше о нём можно здесь .

Шаг 1. Создайте и используйте ViewModelFactory в TasksViewModel

Вы начинаете с обновления классов и тестов, связанных с экраном Tasks .

  1. Откройте TasksViewModel .
  2. Измените конструктор TasksViewModel так, чтобы он принимал TasksRepository вместо его создания внутри класса.

TasksViewModel.kt

// REPLACE
class TasksViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TasksViewModel( private val tasksRepository: TasksRepository ) : ViewModel() { 
    // Rest of class 
}

Поскольку вы изменили конструктор, теперь вам нужно использовать фабрику для создания TasksViewModel . Поместите класс фабрики в тот же файл, что и TasksViewModel , но вы также можете поместить его в отдельный файл.

  1. В нижней части файла TasksViewModel , за пределами класса, добавьте TasksViewModelFactory , который принимает простой TasksRepository .

TasksViewModel.kt

@Suppress("UNCHECKED_CAST")
class TasksViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TasksViewModel(tasksRepository) as T)
}


Это стандартный способ изменения конструкции ViewModel . Теперь, когда у вас есть фабрика, используйте её везде, где вы конструируете свою модель представления.

  1. Обновите TasksFragment для использования фабрики.

TasksFragment.kt

// REPLACE
private val viewModel by viewModels<TasksViewModel>()

// WITH

private val viewModel by viewModels<TasksViewModel> {
    TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Запустите код приложения и убедитесь, что все работает!

Шаг 2. Используйте FakeTestRepository внутри TasksViewModelTest

Теперь вместо использования настоящего репозитория в тестах модели представления вы можете использовать поддельный репозиторий.

  1. Откройте TasksViewModelTest .
  2. Добавьте свойство FakeTestRepository в TasksViewModelTest .

TaskViewModelTest.kt

@RunWith(AndroidJUnit4::class)
class TasksViewModelTest {

    // Use a fake repository to be injected into the viewmodel
    private lateinit var tasksRepository: FakeTestRepository
    
    // Rest of class
}
  1. Обновите метод setupViewModel , чтобы создать FakeTestRepository с тремя задачами, а затем создайте tasksViewModel с этим репозиторием.

TasksViewModelTest.kt

    @Before
    fun setupViewModel() {
        // We initialise the tasks to 3, with one active and two completed
        tasksRepository = FakeTestRepository()
        val task1 = Task("Title1", "Description1")
        val task2 = Task("Title2", "Description2", true)
        val task3 = Task("Title3", "Description3", true)
        tasksRepository.addTasks(task1, task2, task3)

        tasksViewModel = TasksViewModel(tasksRepository)
        
    }
  1. Поскольку вы больше не используете код Androidx Test ApplicationProvider.getApplicationContext , вы также можете удалить аннотация @RunWith(AndroidJUnit4::class) .
  2. Запустите свои тесты, убедитесь, что они все еще работают!

Используя инъекцию зависимости конструктора, вы теперь удалили DefaultTasksRepository в качестве зависимости и заменили его на FakeTestRepository в тестах.

Шаг 3. Также обновите фрагмент и просмотр TaskDetail и ViewModel

Внесите те же изменения для TaskDetailFragment и TaskDetailViewModel . Это подготовит код, когда вы напишите TaskDetail Tests далее.

  1. Откройте TaskDetailViewModel .
  2. Обновите конструктор:

TaskDetailViewModel.kt

// REPLACE
class TaskDetailViewModel(application: Application) : AndroidViewModel(application) {

    private val tasksRepository = DefaultTasksRepository.getRepository(application)

    // Rest of class
}

// WITH

class TaskDetailViewModel(
    private val tasksRepository: TasksRepository
) : ViewModel() { // Rest of class }
  1. В нижней части файла TaskDetailViewModel , вне класса, добавьте TaskDetailViewModelFactory .

TaskDetailViewModel.kt

@Suppress("UNCHECKED_CAST")
class TaskDetailViewModelFactory (
    private val tasksRepository: TasksRepository
) : ViewModelProvider.NewInstanceFactory() {
    override fun <T : ViewModel> create(modelClass: Class<T>) =
        (TaskDetailViewModel(tasksRepository) as T)
}
  1. Обновление TasksFragment , чтобы использовать фабрику.

Tasksfragment.kt

// REPLACE
private val viewModel by viewModels<TaskDetailViewModel>()

// WITH

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}
  1. Запустите свой код и убедитесь, что все работает.

Теперь вы можете использовать FakeTestRepository вместо реального репозитория в TasksFragment и TasksDetailFragment .

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

  • Образец ServiceLocator
  • библиотеки эспрессо и макито

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

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

Шаг 1. Добавить зависимости Градл

  1. Добавьте следующие зависимости Gradle.

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

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "junit:junit:$junitVersion"
    androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutinesVersion"

    // Testing code should not be included in the main code.
    // Once https://issuetracker.google.com/128612536 is fixed this can be fixed.

    implementation "androidx.fragment:fragment-testing:$fragmentVersion"
    implementation "androidx.test:core:$androidXTestCoreVersion"

Эти зависимости включают:

  • junit:junit - JUNIT, который необходим для написания основных тестовых заявлений.
  • androidx.test:core - Core Androidx Test Library
  • kotlinx-coroutines-test -библиотека тестирования Coroutines
  • androidx.fragment:fragment-testing библиотека испытаний androidx для создания фрагментов в тестах и изменения их состояния.

Поскольку вы будете использовать эти библиотеки в вашем наборе источников androidTest , используйте androidTestImplementation , чтобы добавить их в качестве зависимостей.

Шаг 2. Сделайте класс TaskDetailFragmentTest

TaskDetailFragment показывает информацию об одной задаче.

Вы начнете с написания фрагментного теста для TaskDetailFragment , поскольку он имеет довольно базовую функциональность по сравнению с другими фрагментами.

  1. Open taskdetail.TaskDetailFragment .
  2. Создайте тест на TaskDetailFragment , как вы делали раньше. Примите выбор по умолчанию и поместите его в набор исходных AndroidTest (не исходный набор test ).

  1. Добавьте следующие аннотации в класс TaskDetailFragmentTest .

TaskDetailFragmentTest.kt

@MediumTest
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

}

Целью этих аннотаций является:

  • @MediumTest -выполняет тест в качестве интеграционного теста «среднего времени» (по сравнению с @SmallTest модульными тестами и @LargeTest большие сквозные тесты). Это помогает вам группировать и выбрать, какой размер теста для запуска.
  • @RunWith(AndroidJUnit4::class) - используется в любом классе с использованием теста Androidx.

Шаг 3. Запустите фрагмент из теста

В этой задаче вы собираетесь запустить TaskDetailFragment , используя библиотеку тестирования Androidx . FragmentScenario - это класс из теста Androidx, который оборачивается вокруг фрагмента и дает вам прямой контроль над жизненным циклом фрагмента для тестирования. Чтобы написать тесты для фрагментов, вы создаете FragmentScenario для тестирования, который вы тестируете ( TaskDetailFragment ).

  1. Скопируйте этот тест в TaskDetailFragmentTest .

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

Этот код выше:

  • Создает задачу.
  • Создает Bundle , который представляет фрагментные аргументы для задачи, которая передается в фрагмент).
  • Функция launchFragmentInContainer создает FragmentScenario , с этим пакетом и темой.

Это еще не готовый тест, потому что он ничего не утверждает. На данный момент запустите тест и наблюдайте, что происходит.

  1. Это инструментальный тест, поэтому убедитесь, что эмулятор или ваше устройство видны.
  2. Запустите тест.

Несколько вещей должны произойти.

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

Наконец, посмотрите внимательно и обратите внимание, что фрагмент говорит «нет данных», поскольку он не успешно загружает данные задачи.

Ваш тест должен загрузить TaskDetailFragment , что вы сделали), и утверждать, что данные были правильно загружены. Почему нет данных? Это потому, что вы создали задачу, но вы не сохранили ее в репозитории.

    @Test
    fun activeTaskDetails_DisplayedInUi() {
        // This DOES NOT save the task anywhere
        val activeTask = Task("Active Task", "AndroidX Rocks", false)

        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

У вас есть этот FakeTestRepository , но вам нужен какой -то способ заменить ваш настоящий репозиторий вашим фальшивым фрагментом . Вы сделаете это дальше!

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

Вы не можете использовать инъекцию зависимости конструктора здесь, как и раньше, когда вам нужно было обеспечить зависимость от модели представления или репозитория. Инъекция зависимости конструктора требует, чтобы вы построили класс. Фрагменты и действия являются примерами классов, которые вы не строите, и, как правило, не имеют доступа к конструктору.

Поскольку вы не строите фрагмент, вы не можете использовать инъекцию зависимости конструктора, чтобы обмениваться двойным тестом репозитория ( FakeTestRepository ) на фрагмент. Вместо этого используйте шаблон локатора обслуживания . Схема локатора обслуживания является альтернативой инъекции зависимости. Он включает в себя создание класса Singleton под названием «Locator Service Locator», целью которой является обеспечение зависимостей, как для регулярного, так и для тестового кода. В обычном коде приложения ( main набор источников) все эти зависимости являются регулярными зависимостями приложения. Для испытаний вы изменяете локатор сервиса, чтобы предоставить двойные версии зависимостей.

Не используя локатор обслуживания


Использование локатора обслуживания

Для этого приложения CodeLab сделайте следующее:

  1. Создайте класс локатора услуг, который способен строить и хранить репозиторий. По умолчанию он строит «нормальный» репозиторий.
  2. Refactor Your Code так, чтобы, когда вам нужен хранилище, используйте локатор службы.
  3. В своем классе тестирования вызовите метод в локаторе обслуживания, который заменяет «нормальный» репозиторий с вашим тестовым двойным.

Шаг 1. Создайте ServiceLocator

Давайте сделаем класс ServiceLocator . Он будет жить в основном наборе источников с остальной частью кода приложения, потому что он используется основным кодом приложения.

Примечание. ServiceLocator - это синглтон, поэтому используйте ключевое слово Kotlin object для класса.

  1. Создайте файл ServiceLocator.kt в верхнем уровне основного набора источников.
  2. Определите object , называемый ServiceLocator .
  3. Создайте переменные экземпляра database и repository и установите как на null .
  4. Аннотируйте репозиторий @Volatile , потому что он может быть использован несколькими потоками ( @Volatile подробно объясняется здесь ).

Ваш код должен выглядеть как показано ниже.

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

}

Сейчас единственное, что нужно сделать вашему ServiceLocator , это знать, как вернуть TasksRepository . Это вернет ранее существовавшего DefaultTasksRepository или при необходимости вернет и вернет и вернет новую DefaultTasksRepository .

Определите следующие функции:

  1. provideTasksRepository - либо предоставляет уже существующий репозиторий, либо создает новый. Этот метод должен быть synchronized по this , чтобы избежать, в ситуациях с несколькими потоками, когда -либо случайно создавая два экземпляра репозитория.
  2. createTasksRepository - код создания нового репозитория. Будет называть createTaskLocalDataSource и создаст новый TasksRemoteDataSource .
  3. createTaskLocalDataSource - код создания нового локального источника данных. Вызовет createDataBase .
  4. createDataBase - код для создания новой базы данных.

Заполненный код ниже.

ServiceLocator.kt

object ServiceLocator {

    private var database: ToDoDatabase? = null
    @Volatile
    var tasksRepository: TasksRepository? = null

    fun provideTasksRepository(context: Context): TasksRepository {
        synchronized(this) {
            return tasksRepository ?: createTasksRepository(context)
        }
    }

    private fun createTasksRepository(context: Context): TasksRepository {
        val newRepo = DefaultTasksRepository(TasksRemoteDataSource, createTaskLocalDataSource(context))
        tasksRepository = newRepo
        return newRepo
    }

    private fun createTaskLocalDataSource(context: Context): TasksDataSource {
        val database = database ?: createDataBase(context)
        return TasksLocalDataSource(database.taskDao())
    }

    private fun createDataBase(context: Context): ToDoDatabase {
        val result = Room.databaseBuilder(
            context.applicationContext,
            ToDoDatabase::class.java, "Tasks.db"
        ).build()
        database = result
        return result
    }
}

Шаг 2. Используйте ServiceLocator в приложении

Вы собираетесь внести изменения в свой основной код приложения (не ваши тесты), чтобы создать репозиторий в одном месте, ваш ServiceLocator .

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

  1. На верхнем уровне иерархии пакетов откройте для вашего TodoApplication откройте для вашего репозитория и назначьте его val , который получается с использованием ServiceLocator.provideTaskRepository .

TodoApplication.kt

class TodoApplication : Application() {

    val taskRepository: TasksRepository
        get() = ServiceLocator.provideTasksRepository(this)

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Timber.plant(DebugTree())
    }
}

Теперь, когда вы создали репозиторий в приложении, вы можете удалить старый метод getRepository в DefaultTasksRepository .

  1. Откройте DefaultTasksRepository и удалите компаньон.

Defaulttasksrepository.kt

// DELETE THIS COMPANION OBJECT
companion object {
    @Volatile
    private var INSTANCE: DefaultTasksRepository? = null

    fun getRepository(app: Application): DefaultTasksRepository {
        return INSTANCE ?: synchronized(this) {
            val database = Room.databaseBuilder(app,
                ToDoDatabase::class.java, "Tasks.db")
                .build()
            DefaultTasksRepository(TasksRemoteDataSource, TasksLocalDataSource(database.taskDao())).also {
                INSTANCE = it
            }
        }
    }
}

Теперь, где бы вы ни использовали getRepository , используйте вместо этого taskRepository приложения. Это гарантирует, что вместо того, чтобы делать репозиторий напрямую, вы получаете любое хранилище, предоставляемое ServiceLocator .

  1. Откройте TaskDetailFragement getRepository
  2. Замените этот вызов вызовом, который получает репозиторий от TodoApplication .

TaskDetailFragment.kt

// REPLACE this code
private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
}

// WITH this code

private val viewModel by viewModels<TaskDetailViewModel> {
    TaskDetailViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
}
  1. Сделайте то же самое для TasksFragment .

Tasksfragment.kt

// REPLACE this code
    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory(DefaultTasksRepository.getRepository(requireActivity().application))
    }


// WITH this code

    private val viewModel by viewModels<TasksViewModel> {
        TasksViewModelFactory((requireContext().applicationContext as TodoApplication).taskRepository)
    }
  1. Для StatisticsViewModel и AddEditTaskViewModel обновите код, который приобретает репозиторий для использования репозитория из TodoApplication .

Tasksfragment.kt

// REPLACE this code
    private val tasksRepository = DefaultTasksRepository.getRepository(application)



// WITH this code

    private val tasksRepository = (application as TodoApplication).taskRepository

  1. Запустите свое приложение (не тест)!

Поскольку вы только реформировались, приложение должно работать так же без проблем.

Шаг 3. Создать fakeAndroidtestrepository

У вас уже есть FakeTestRepository в наборе источников испытаний. Вы не можете поделиться классами тестирования между исходными наборами test и androidTest по умолчанию. Таким образом, вам необходимо сделать дублированный класс FakeTestRepository в исходном наборе androidTest и назвать его FakeAndroidTestRepository .

  1. Щелкните правой кнопкой мыши набор источников androidTest и сделайте пакет данных . Еще раз щелкните правой кнопкой мыши и сделайте исходный пакет.
  2. Сделайте новый класс в этом исходном пакете под названием FakeAndroidTestRepository.kt .
  3. Скопируйте следующий код в этот класс.

FakeAndroidtestrepository.kt

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import com.example.android.architecture.blueprints.todoapp.data.Result
import com.example.android.architecture.blueprints.todoapp.data.Result.Error
import com.example.android.architecture.blueprints.todoapp.data.Result.Success
import com.example.android.architecture.blueprints.todoapp.data.Task
import kotlinx.coroutines.runBlocking
import java.util.LinkedHashMap



class FakeAndroidTestRepository : TasksRepository {

    var tasksServiceData: LinkedHashMap<String, Task> = LinkedHashMap()

    private var shouldReturnError = false

    private val observableTasks = MutableLiveData<Result<List<Task>>>()

    fun setReturnError(value: Boolean) {
        shouldReturnError = value
    }

    override suspend fun refreshTasks() {
        observableTasks.value = getTasks()
    }

    override suspend fun refreshTask(taskId: String) {
        refreshTasks()
    }

    override fun observeTasks(): LiveData<Result<List<Task>>> {
        runBlocking { refreshTasks() }
        return observableTasks
    }

    override fun observeTask(taskId: String): LiveData<Result<Task>> {
        runBlocking { refreshTasks() }
        return observableTasks.map { tasks ->
            when (tasks) {
                is Result.Loading -> Result.Loading
                is Error -> Error(tasks.exception)
                is Success -> {
                    val task = tasks.data.firstOrNull() { it.id == taskId }
                        ?: return@map Error(Exception("Not found"))
                    Success(task)
                }
            }
        }
    }

    override suspend fun getTask(taskId: String, forceUpdate: Boolean): Result<Task> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        tasksServiceData[taskId]?.let {
            return Success(it)
        }
        return Error(Exception("Could not find task"))
    }

    override suspend fun getTasks(forceUpdate: Boolean): Result<List<Task>> {
        if (shouldReturnError) {
            return Error(Exception("Test exception"))
        }
        return Success(tasksServiceData.values.toList())
    }

    override suspend fun saveTask(task: Task) {
        tasksServiceData[task.id] = task
    }

    override suspend fun completeTask(task: Task) {
        val completedTask = Task(task.title, task.description, true, task.id)
        tasksServiceData[task.id] = completedTask
    }

    override suspend fun completeTask(taskId: String) {
        // Not required for the remote data source.
        throw NotImplementedError()
    }

    override suspend fun activateTask(task: Task) {
        val activeTask = Task(task.title, task.description, false, task.id)
        tasksServiceData[task.id] = activeTask
    }

    override suspend fun activateTask(taskId: String) {
        throw NotImplementedError()
    }

    override suspend fun clearCompletedTasks() {
        tasksServiceData = tasksServiceData.filterValues {
            !it.isCompleted
        } as LinkedHashMap<String, Task>
    }

    override suspend fun deleteTask(taskId: String) {
        tasksServiceData.remove(taskId)
        refreshTasks()
    }

    override suspend fun deleteAllTasks() {
        tasksServiceData.clear()
        refreshTasks()
    }

   
    fun addTasks(vararg tasks: Task) {
        for (task in tasks) {
            tasksServiceData[task.id] = task
        }
        runBlocking { refreshTasks() }
    }
}

Шаг 4. Подготовьте свой ServiceLocator для испытаний

Хорошо, время для использования ServiceLocator для обмена тестовыми удвоениями при тестировании. Для этого вам нужно добавить код в свой код ServiceLocator .

  1. Open ServiceLocator.kt .
  2. Отметьте сеттер для tasksRepository как @VisibleForTesting . Эта аннотация является способом выразить, что причина, по которой сеттер публичной, заключается в тестировании.

ServiceLocator.kt

    @Volatile
    var tasksRepository: TasksRepository? = null
        @VisibleForTesting set

Независимо от того, запускаете ли вы свой тест в одиночку или в группе тестов, ваши тесты должны работать точно так же. Это означает, что ваши тесты не должны иметь поведения, которое зависит друг от друга (что означает избегать обмена объектами между тестами).

Поскольку ServiceLocator является синглтоном, он имеет возможность случайно разделиться между тестами. Чтобы избежать этого, создайте метод, который должным образом сбрасывает состояние ServiceLocator между тестами.

  1. Добавьте переменную экземпляра, называемую lock с Any значением.

ServiceLocator.kt

private val lock = Any()
  1. Добавьте метод, специфичный для тестирования, называемый resetRepository , который очищает базу данных и устанавливает как репозиторий, так и базу данных в NULL.

ServiceLocator.kt

    @VisibleForTesting
    fun resetRepository() {
        synchronized(lock) {
            runBlocking {
                TasksRemoteDataSource.deleteAllTasks()
            }
            // Clear all data to avoid test pollution.
            database?.apply {
                clearAllTables()
                close()
            }
            database = null
            tasksRepository = null
        }
    }

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

На этом этапе вы используете ServiceLocator .

  1. Open TaskDetailFragmentTest .
  2. Объявите переменную lateinit TasksRepository .
  3. Добавьте настройку и метод разрыва, чтобы настроить FakeAndroidTestRepository перед каждым тестом и очистить его после каждого теста.

TaskDetailFragmentTest.kt

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }
  1. Оберните корпус функции activeTaskDetails_DisplayedInUi() в runBlockingTest .
  2. Сохраните activeTask в репозитории перед запуском фрагмента.
repository.saveTask(activeTask)

Окончательный тест выглядит как этот код ниже.

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }
  1. Аннотируйте весь класс с помощью @ExperimentalCoroutinesApi .

Когда закончите, код будет выглядеть так.

TaskDetailFragmentTest.kt

@MediumTest
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }


    @Test
    fun activeTaskDetails_DisplayedInUi()  = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

    }

}
  1. Запустите тест activeTaskDetails_DisplayedInUi() .

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


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

Эспрессо помогает вам:

  • Взаимодействуйте с представлениями, например, кнопки щелчка, скольжение стержня или прокрутка экрана.
  • Утверждают, что определенные представления находятся на экране или находятся в определенном состоянии (например, содержащий конкретный текст, или что флажок проверяется и т. Д.).

Шаг 1. Примечание Gradle зависимость

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

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

dependencies {

  // ALREADY in your code
    androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
   
 // Other dependencies
}

androidx.test.espresso:espresso-core -эта основная зависимость эспрессо включается по умолчанию, когда вы создаете новый проект Android. Он содержит базовый код тестирования для большинства представлений и действий на них.

Шаг 2. Выключите анимацию

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

Для тестирования пользовательского интерфейса Espresso наилучшим образом выключить анимацию (также ваш тест будет работать быстрее!):

  1. На вашем устройстве тестирования перейдите в «Настройки»> «Параметры разработчика» .
  2. Отключите эти три настройки: шкала анимации окон , шкала анимации перехода и шкала продолжительности аниматора .

Шаг 3. Посмотрите на тест на эспрессо

Прежде чем написать тест на эспрессо, посмотрите на код эспрессо.

onView(withId(R.id.task_detail_complete_checkbox)).perform(click()).check(matches(isChecked()))

Этот оператор делает представление на флажке с идентификатором task_detail_complete_checkbox , нажимает его, а затем утверждает, что он проверяется.

Большинство заявлений эспрессо состоят из четырех частей:

1. Статический метод эспрессо

onView

onView является примером статического метода эспрессо, который запускает оператор эспрессо. onView является одним из самых распространенных, но есть и другие варианты, такие как onData .

2. ViewMatcher

withId(R.id.task_detail_title_text)

withId является примером ViewMatcher , который получает представление по своему идентификатору. Есть и другие совместные представления, которые вы можете посмотреть в документации .

3. ViewAction

perform(click())

Метод perform , который принимает ViewAction . ViewAction - это то, что можно сделать с представлением, например, здесь нажимает на представление.

4. ViewAssertion

check(matches(isChecked()))

check , что принимает ViewAssertion . ViewAssertion S Проверьте или утверждает что -то о представлении. Наиболее распространенным ViewAssertion которое вы используете, является утверждение matches . Чтобы закончить утверждение, используйте другого ViewMatcher , в данном случае isChecked .

Обратите внимание, что вы не всегда звоните оба perform и check оператор Espresso. У вас могут быть заявления, которые просто делают утверждение, используя check , или просто выполнять ViewAction с помощью perform .

  1. Open TaskDetailFragmentTest.kt .
  2. Обновите тест activeTaskDetails_DisplayedInUi .

TaskDetailFragmentTest.kt

    @Test
    fun activeTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add active (incomplete) task to the DB
        val activeTask = Task("Active Task", "AndroidX Rocks", false)
        repository.saveTask(activeTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(activeTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Active Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(not(isChecked())))
    }

Вот импортные заявления, если это необходимо:

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.core.IsNot.not
  1. Все после // THEN в комментарии используется эспрессо. Изучите структуру тестирования и использование withId и проверьте, чтобы сделать утверждения о том, как должна выглядеть страница с подробной информацией.
  2. Запустите тест и подтвердите, что он проходит.

Шаг 4. Необязательно, напишите свой собственный тест на эспрессо

Теперь напишите тест самостоятельно.

  1. Создайте новый тест под названием completedTaskDetails_DisplayedInUi и скопируйте этот код скелета.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
       
        // WHEN - Details fragment launched to display task
        
        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
}
  1. Глядя на предыдущий тест, завершите этот тест.
  2. Запустите и подтвердите тестовые проходы.

Завершенная completedTaskDetails_DisplayedInUi должна выглядеть как этот код.

TaskDetailFragmentTest.kt

    @Test
    fun completedTaskDetails_DisplayedInUi() = runBlockingTest{
        // GIVEN - Add completed task to the DB
        val completedTask = Task("Completed Task", "AndroidX Rocks", true)
        repository.saveTask(completedTask)

        // WHEN - Details fragment launched to display task
        val bundle = TaskDetailFragmentArgs(completedTask.id).toBundle()
        launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.AppTheme)

        // THEN - Task details are displayed on the screen
        // make sure that the title/description are both shown and correct
        onView(withId(R.id.task_detail_title_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_title_text)).check(matches(withText("Completed Task")))
        onView(withId(R.id.task_detail_description_text)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_description_text)).check(matches(withText("AndroidX Rocks")))
        // and make sure the "active" checkbox is shown unchecked
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isDisplayed()))
        onView(withId(R.id.task_detail_complete_checkbox)).check(matches(isChecked()))
    }

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

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

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

Вот код в TasksFragment , который перемещается на экран детализации задачи, когда он нажимается.

Tasksfragment.kt

private fun openTaskDetails(taskId: String) {
    val action = TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment(taskId)
    findNavController().navigate(action)
}


Навигация происходит из -за вызова метода navigate . Если вам нужно было написать утверждение Assert, нет простого способа проверить, перейти ли вы в TaskDetailFragment . Навигация - это сложное действие, которое не приводит к четкому выходу или изменению состояния, помимо инициализации TaskDetailFragment .

Вы можете утверждать, что метод navigate был вызван с правильным параметром действия. Это именно то, что делает макет -тест -двойник - он проверяет, были ли вызваны конкретные методы.

Mockito - это структура для создания тестовых удвоений. В то время как слово макет используется в API и имени, это не для простого макета. Это также может сделать заглушки и шпионы.

Вы будете использовать Mockito для создания макетного NavigationController , который может утверждать, что метод навигации был вызван правильно.

Шаг 1. Добавить зависимости Градл

  1. Добавьте зависимости Gradle.

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

    // Dependencies for Android instrumented unit tests
    androidTestImplementation "org.mockito:mockito-core:$mockitoVersion"

    androidTestImplementation "com.linkedin.dexmaker:dexmaker-mockito:$dexMakerVersion" 

    androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"



  • org.mockito:mockito-core -это зависимость от макета.
  • dexmaker-mockito -эта библиотека должна использовать Mockito в проекте Android. Mockito должен генерировать классы во время выполнения. На Android это делается с использованием кода Dex Byte, и поэтому эта библиотека позволяет Mockito генерировать объекты во время выполнения на Android.
  • androidx.test.espresso:espresso-contrib -эта библиотека состоит из внешних вкладов (отсюда и название), которые содержат код тестирования для более продвинутых представлений, таких как DatePicker и RecyclerView . Он также содержит проверки доступности и класс под названием CountingIdlingResource , который охватывается позже.

Шаг 2. Создайте TasksFragmentTest

  1. Открытые TasksFragment .
  2. Щелкните правой кнопкой мыши имени класса TasksFragment и выберите «Создать» , затем тестируйте . Создайте тест в исходном наборе AndroidTest .
  3. Скопируйте этот код в TasksFragmentTest .

Tasksfragmenttest.kt

@RunWith(AndroidJUnit4::class)
@MediumTest
@ExperimentalCoroutinesApi
class TasksFragmentTest {

    private lateinit var repository: TasksRepository

    @Before
    fun initRepository() {
        repository = FakeAndroidTestRepository()
        ServiceLocator.tasksRepository = repository
    }

    @After
    fun cleanupDb() = runBlockingTest {
        ServiceLocator.resetRepository()
    }

}

Этот код выглядит похоже на код TaskDetailFragmentTest , который вы написали. Он устанавливается и разрушает FakeAndroidTestRepository . Добавьте навигационный тест, чтобы проверить, что когда вы нажимаете на задачу в списке задач, он доставит вас к правильной TaskDetailFragment .

  1. Добавьте тест clickTask_navigateToDetailFragmentOne .

Tasksfragmenttest.kt

    @Test
    fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
        repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
        repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        
    }
  1. Используйте mock функцию Mockito, чтобы создать макет.

Tasksfragmenttest.kt

 val navController = mock(NavController::class.java)

Чтобы высмеивать в Макето, пройдите в тот класс, который вы хотите издеваться.

Затем вам нужно связать свой NavController с фрагментом. onFragment позволяет вам вызывать методы на самом фрагменте.

  1. Сделайте свой новый макет фрагментом NavController .
scenario.onFragment {
    Navigation.setViewNavController(it.view!!, navController)
}
  1. Добавьте код, чтобы нажать на элемент в RecyclerView , который имеет текст «Title1».
// WHEN - Click on the first list item
        onView(withId(R.id.tasks_list))
            .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
                hasDescendant(withText("TITLE1")), click()))

RecyclerViewActions является частью библиотеки espresso-contrib и позволяет выполнять действия эспрессо на переработке .

  1. Убедитесь, что navigate была вызвана с правильным аргументом.
// THEN - Verify that we navigate to the first detail screen
verify(navController).navigate(
    TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")

Метод verify Mockito - это то, что делает этот макет - вы сможете подтвердить высмеивающий navController , который называется конкретным методом ( navigate ) с параметром ( actionTasksFragmentToTaskDetailFragment с идентификатором «id1»).

Полный тест выглядит так:

@Test
fun clickTask_navigateToDetailFragmentOne() = runBlockingTest {
    repository.saveTask(Task("TITLE1", "DESCRIPTION1", false, "id1"))
    repository.saveTask(Task("TITLE2", "DESCRIPTION2", true, "id2"))

    // GIVEN - On the home screen
    val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
    
                val navController = mock(NavController::class.java)
    scenario.onFragment {
        Navigation.setViewNavController(it.view!!, navController)
    }

    // WHEN - Click on the first list item
    onView(withId(R.id.tasks_list))
        .perform(RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
            hasDescendant(withText("TITLE1")), click()))


    // THEN - Verify that we navigate to the first detail screen
    verify(navController).navigate(
        TasksFragmentDirections.actionTasksFragmentToTaskDetailFragment( "id1")
    )
}
  1. Запустите свой тест!

Таким образом, для проверки навигации вы можете:

  1. Используйте Mockito, чтобы создать макет NavController .
  2. Прикрепите этот высмеивающий NavController к фрагменту.
  3. Убедитесь, что навигация была вызвана с правильным действием и параметрами.

Шаг 3. Необязательно, напишите ClickAddtaskButton_NavigateToadditTragment

Чтобы увидеть, сможете ли вы написать навигационный тест самостоятельно, попробуйте эту задачу.

  1. Напишите тест clickAddTaskButton_navigateToAddEditFragment , который проверяет, что если вы нажмете на + FAB, вы перемещаетесь в AddEditTaskFragment .

Ответ ниже.

Tasksfragmenttest.kt

    @Test
    fun clickAddTaskButton_navigateToAddEditFragment() {
        // GIVEN - On the home screen
        val scenario = launchFragmentInContainer<TasksFragment>(Bundle(), R.style.AppTheme)
        val navController = mock(NavController::class.java)
        scenario.onFragment {
            Navigation.setViewNavController(it.view!!, navController)
        }

        // WHEN - Click on the "+" button
        onView(withId(R.id.add_task_fab)).perform(click())

        // THEN - Verify that we navigate to the add screen
        verify(navController).navigate(
            TasksFragmentDirections.actionTasksFragmentToAddEditTaskFragment(
                null, getApplicationContext<Context>().getString(R.string.add_task)
            )
        )
    }

Нажмите здесь, чтобы увидеть различие между начальным кодом, и окончательным кодом.

Чтобы загрузить код для готового CodeLab, вы можете использовать команду GIT ниже:

$ git clone https://github.com/googlecodelabs/android-testing.git
$ cd android-testing
$ git checkout end_codelab_2


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

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

Этот CodeLab освещал, как настроить ручную инъекцию зависимости, локатор сервиса и как использовать подделки и макет в ваших приложениях Android Kotlin. В частности:

  • То, что вы хотите проверить, и ваша стратегия тестирования определяет, какие тесты вы собираетесь реализовать для своего приложения. Модульные тесты сосредоточены и быстрые. Интеграционные тесты проверяют взаимодействие между частями вашей программы. Тестовые тесты проверяют функции, имеют самую высокую точность, часто приводятся в силу и могут занять больше времени.
  • Архитектура вашего приложения влияет на то, как сложно проверить.
  • Разработка TDD или тестового управления - это стратегия, в которой вы сначала пишете тесты, а затем создаете функцию для прохождения тестов.
  • Чтобы изолировать части вашего приложения для тестирования, вы можете использовать тестовые удвоения. Тестовый двойник - это версия класса, созданного специально для тестирования. Например, вы фальшиво получаете данные из базы данных или в Интернете.
  • Используйте инъекцию зависимостей , чтобы заменить реальный класс на класс тестирования, например, репозиторий или сетевой слой.
  • Используйте немаловатое тестирование ( androidTest ) для запуска компонентов пользовательского интерфейса.
  • Когда вы не можете использовать инъекцию зависимости конструктора, например, для запуска фрагмента, вы часто можете использовать локатор услуг. Схема локатора обслуживания является альтернативой инъекции зависимости. Он включает в себя создание класса Singleton под названием «Locator Service Locator», целью которой является обеспечение зависимостей, как для регулярного, так и для тестового кода.

Курс Udacity:

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

Видео:

Другой:

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