# UI-тесты в Android в Маркете

## Схема работы тестов

![alt text](https://jing.yandex-team.ru/files/apopsuenko/Untitled%20Diagram%20%282%29.29139d6.png)

### Структура пакетов

- `di` - здесь хранятся модули, для подмены продовских классов в тестах
- `mocks` - храним стейты и моки для подмены серверных ответов
- `precondition` - храняятся общие для тестов действия
- `scenario` - храниятся сами тесты-сценарии
- `screen` - хранятся `PageObject` для экранов
- `test` - утилитные классы для инфры тестов

### Инфра по запуску тестов

UI-тесты работают на `JUnit` и, соответственно, классы и принципы те же.

Основные классы:
- `MarketTestInstrumentationRunner` - класс, который запускает все тесты, создаёт инстанс приложения. Это первая точка входа в исполнение теста
- `TestCaseJUnit4ClassRunner` - класс, который выполняет конкретный тест. За счёт этого инкапсулирует в себе логику по сбору информации о тесте
- `TestCase` - базовый класс для всех тестов. Он инкапсулирует логику по запуску теста, выполнению шагов, задаёт дефолтные стейты и многое другое

### Проверки

Проверки вьюх на данные и состояния происходят по паттерну [PageObject](https://medium.com/tech-tajawal/page-object-model-pom-design-pattern-f9588630800b).
Этот паттерн позволяет инкапсулировать логику по проверке состояния экрана в неком классе. В итоге тест не знает о том, какие вьюхи есть на экране и у него нет доступа к ним. Тест просто говорит что-то классу, который отвечает за экран в тестах.

Сами проверки происходят через [Espresso](https://developer.android.com/training/testing/espresso). Это фреймворк от гугла для ui-тестов. Но сам фреймворк выглядит не очень приятно.
Поэтому используется DSL-обёртка над ним [Kakao](https://github.com/agoda-com/Kakao). В этой обёртке уже есть поддержка паттерна `PageObject`.

Собственные (кастомные) проверки кладём в пакет `test/kakao/matchers`.
Также там содержится много дополнительного, что связано с взаимодействием с `Kakao`

### Мокирование

Перед тем, как внедрить способ мокирования, рассматривались варианты и каждый из них имел плюсы и минусы:

- json-файлы с ответами:

(+) легко создавать

(+) понятно и виден весь ответ

(+) тестируется парсинг

(-) поддержка при изменении ответа

(-) сложно записывать и потом воспроизводить корнер-кейсы

(-) нужна структура по хранению/доставанию нужных файлов для тестов

- подмена dto в сетевом слое

(+) легко поддерживать

(+) легко делать корнер-кейсы

(+) можно видеть необходиое состояние прямо в тесте

(+) можно сделать прослойку и маппить не только в dto, а и в другие виды моков

(-) не тестируется парсинг

(-) сложные маппинги

- поход в тестинг

(+) тестируются не только все слои приложения, но и взаимодейстие с сервером

(+) можно использовать готовый набор товаров

(-) сложно проверять корнер-кейсы

(-) сложно подготавливать нужное состояние приложения

(-) флакание данных/сети

(-) почти всегда сломанный тестинг

- [кадавр](https://wiki.yandex-team.ru/users/b-vladi/kadavrik/)

(+) тестируется не только сетевой слой, но и связка с ФАПИ/КАПИ

(+) используется фронтами

(+) умные стейты (добавил в корзину и при запросе корзины в ней будет тот товар)

(-) флакание сети

(-) написание новых стейтов на JS

В итоге был выбран подход.
Были написаны некие **бизнес-классы стейты**, которые мокировали данные от сервера, путём маппинга в `dto`. Эти классы получаются более лаконичными, чем `dto`, поэтому не выглядят монструозными.
При этом сохраняются все плюсы подмены `dto` в сетевом слое.

Но, этот подход также можно использовать для маппинга стейтов в стейты на кадавре. Тем самым можно поддерживать 2 вида мокирования или легко переехать на кадавр, если появится необходимость.

#### Правила мокирования

После 2-х лет написания тестов стало понятно, как их можно улучшить.

Проблемы:
1. различные стейты приходилось связывать между собой
2. стейты хранят данные, не пригодные для анализа тестов (названия вида "name", "text", "value")
3. Нет общего стейта, на основании которого бы приложение работало так, как будто оно в тестинге (данных мало, но они есть)

Исходя из этого сформулировались правила:
1. Представить стейты в виде классов с дефолтными значениями (как будто таблица с записями)
2. Связать стейты между собой по-умолчанию (как будто таблицы нормализованны и связанны)
3. В тесте не создавать новые стейты и проверять функционал по ним, а открывать категории/выдачи/товары и другое для уже созданных стейтов, в которых есть функционал, нужный для проверки
4. Для того, чтобы приложение выглядело нормально даже в тестах, поломки искались проще, в стейтах задавать нормальные, читаемые надписи и значения
5. Ссылаться на другие моки по `id`. `Id` в моке задаётся через `generateIntId()` (`generateStringId()`, `generateLongId()`)

#### Мокирование других данных

Помимо мокирования ответов сервера можно замокать:
- БД (`DatabaseStateStore`)
- префы (`PrefsStateStore`)
- фич-тоглы (`FeatureConfigState`)
- экспы (`ExperimentState`)

#### Подмена на строне DI

Для того, чтобы не дёргать запросы, не вызывать библиотеки и проверять использование библиотек, подменяем DI:
- отправка метрики
- авторизация
- `firebase`
- другие

#### Мапперы

Для маппинга стейтов в dto используются мапперы.
Они лежат в пакете `mocks/local/mapper`.

{% note warning %}

Мапперы нужно писать, как продакшн код. В разных классах, удобочитаемо и т.д.

{% endnote %}

### Ассинхронные задачи
У нас подключен `rx-idle`, кототрый стопает все ui-проверки, пока не завершились все rx-потоки. Решение в лоб. В дальнейшем попробуем перейти на схему проверки каждые n миллисекунд с таймаутом.

### Хуки
Для того, чтобы сделать что-то до ввыполнения теста, нужно юзать теструлы. Они расположены в пакете `test/rule`.

Также есть рул, который позволяет обратиться к открытой активити. Пример - `ActivityFinishedRule`.

Может есть техники получше, но пока я их не знаю.

## Написание тестов

### Процесс

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

[Пальма](https://testpalm.yandex-team.ru/) (`TestPalm`) - сервис для заведения кейсов, тест-ранов и всего такого.

Попроси QA в контуре или дежурного QA помочь с тесткейсом, если у тебя нет готового.
Для матчинга кейсов и тестов в коде есть аннотация `@TestCaseId`

### Правила

У тестов должны быть:
- аннотация `@Epic`, которая указывает, какую функциональность проверяет тест
- аннотация `@TestCaseId`, в которой указан `id` теста в пальме
- аннотация `@Feature` (опционально), которая указывает на конкретную часть функциональности

Тест должен:
- наследоваться от класса `TestCase`
- иметь описание
- иметь шаги выполнения
- все константы и другое должны храниться в `companion object` с уровнем доступа `private`

`action` - блок кода, в котором выполняется действие, чтобы выполнить дальнейшие шаги и проверки теста

`expectations` - блок кода, в котором производятся проверки, что для пользователя отображается то, что мы ожидаем увидеть

{% note alert %}

Не нужно путать эти блоки и делать проверки в `action` или действия в `expectations`.
Это тоже код и он должен быть читаемым, понятным и соблюдать правила

{% endnote %}

### Как написать тест

#### Генерация тестового класса

Для того, чтобы быстро сгенерить тестовый класс на основе тест-кейса нужно выполнить скрипт `create_ui_test.sh`.
Следовать инструкциям и вам сгенерится каркас теста.

#### Подготовка стейтов

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

#### Подготовка экрана

Если для теста нужен экран, которого нет в пакете `screen`, то нужно создать новый по бразу других экранов.

Правила:
1. Набор вьюх, их логика, их расположение и другое должны быть инкапсулированы в классе экрана
2. Из теста нельзя получить доступ к вьюхам экрана

### Как запустить тесты

1. Все тесты можно запустить на CI. Это будет быстрее и не затратно по ресурсам компьютера
2. Тесты для эпика запускаются в пайплайне автоматически
3. Тесты запускаются [так же](https://docs.yandex-team.ru/market-mobile/android/tests/unit-tests#kak-zapuskat), как и `unit`-ы

### Как дебажить и чинить тесты

Тесты выполняются в нескольких потоках:
1. Поток выполнения теста
2. Потоки приложения

Если поставить точку останова на каком-нибудь шаге в тесте и не продолжать этот поток, а переключаться на потоки приложения, то можно дебажить код приложения.
Также можно дебажить код мапперов, например.

Если падает тест, то:
1. Нужно понять, на каком шаге он падает
2. Можно посмотреть логи, в них могут быть записаны ошибки маппинга данных
3. Проверять локально тест нужно на таком же девайсе, на каком тесты выполняются на CI

### Возможные проблемы

1. Ошибки бизнес-логики могут поглощаться оператором `onErrorReturnItem/onErrorResumeNext`
2. Во время скорола действие `click()` поглощается `ScrollView/NestedScrollView/RecyclerView` для остановки скролла (вывод - не надо скроллить внутри `handler.post { ... }`)
3. Внутри реализации `ViewAssertion` нельзя вызывать другие `assertion`, это приведет к дедлоку
4. В матчер для вьюх дочерних к `KRecyclerView` обязательно надо пробрасывать `parent`.
5. Тест могут не запускаться, если в скрине при инициализации `View` ищет себя в `RecyclerView`, помогает вынести эту `View` в `lazy`
6. При локальном запуске автотестов может появится ошибка "No tests were found". В таком случае попробуй запустить на другом API level, например, 25 или 27 (в зависимости от того, на каком упали в CI).

## Отчёт

При прогоне тестов, генерится [Allure](http://allure.qatools.ru/)-отчёт.

В отчёте вы можете посмотреть:
1. Вкладка `Overview`. Общие данные по прогону, сколько тестов прошло, сколько - не прошло
2. Вкладка `Graphs`. Время выполнения тестов.
3. Вкладка `Timeline`. Порядок выполнения тестов с возможностью фильтрации по временивыполнения
4. Вкладка `Behaviors`. Список тестов с группировкой по функционалу, фильтрацией по статусу и др
5. Вкладка `Packages`. Список тестов с группировкой по пакету

Карточка упавшего теста представляет собой особый интерес.
![alt text](https://jing.yandex-team.ru/files/apopsuenko/2021-08-04T13:24:58Z.8e6f00f.png)

На карточке мы можем увидеть:
1. Путь к классу теста
2. Имя и статус теста
3. Вкладки

3.1. `Overview`
- ошибка прогона
- ссылка на тест-кейс в пальме
- скриншот шага, на котором тест упал
- иерархия вьюх на момент падения в странном формате, но хоть что-то
- лог на момент падения

3.2. `History`
Пока пусто, чтобы заинтегрировать, нужно много сил

3.3. `Retries`
На вкладке можно увидеть попытки, сделанные для прогона теста. Тест может быть зелёным, но у него могут быть неуспешщные попытки

## Тесты на CI

Для прогона тестов используется фреймворк [Frankenstein](https://wiki.yandex-team.ru/YandexMobile/frankenstein/)
[Инструкция по настройке](https://wiki.yandex-team.ru/YandexMobile/frankenstein/matras-new/)

[Пример таски](https://sandbox.yandex-team.ru/task/589609987/view)
`GENERIC_TESTS_ANDROID_LAUNCHER` - общая таска на тесты, она создаёт подтаски в зависимости от того, на каких платформах нужно гонять тесты
`GENERIC_TESTS_ANDROID_RUNNER` - таска на прогон на конкретной платформе

В общей таске запускается создание аггрегированного отчёта на основе дочерних.

Параметры, которые мы задаём:
- `vcs_url` - репозиторий
- `branch` - ветка, на которой гоняем тесты
- `commit` - хеш коммита
- `ssh_key` - путь к ssh ключу (там лежит ключ робота) (лежит в секретнице сендбокса)
- `gradle_parameters: environment.maxParallelForks: 4` - запуск на 4 эмуляторах в каждой платформе (сильно сокращает время прогона тестов)
- `secret_gradle_parameters` - `yav.oauth.token: BERU_ANDROID:yav_oauth_token` - токен к яндексовой секретнице (лежит в секретнице сендбокса)
- `system_gradle_parameters` - `false`, потому что мы грузим тексты из Танкера (сервис для хранения строк) и иногда сборка может уйти вперёд и падать, так как тексты ещё не скачены (можно оставить `true`)
- `output_dir_param_name` - `reportsOutputDir` имя параметра, где будут лежать выходные файлы в задаче (этот параметр юзаем в наших `gradle`-скриптах, чтобы что-то посмотреть или записать туда)
- `reports` - пути к публикуемым отчётам, чтобы они отображались в UI таски
- `build_target` - `gradle`-таска для сборки проекта и тестов (на случай, если сборка проекта и сборка тестов в разных тасках)
- `run_target` - `gradle`-таска для запуска тестов (вынесена отдельно от таски сборки потому, что можно собирать проект 1 раз и потом прогонять на разных платформах без сборки) (мы пока такое не реализовали, потому что будет выйгрыш в потребляемых ресурсах, а вот скорости прогона - нет)
- `report_target` - `gradle`-таска генерации отчётов
- `report_duration` - примерная длительность таски на генерацию отчётов (нужно, чтобы лучше рассчитывать квоту)
- `aggregated_report_task` - таска на генерацию аггрегированного отчёта
- `tasks_reports_dir_param_name` - имя параметра, пот которому можно найти папку с отчётами в `gradle`-скриптах
- `aggregated_reports_dir_param_name` - имя параметра, пот которому можно найти папку с агрегированными отчётами в `gradle`-скриптах
- `aggregated_reports` - Имя и путь к агрегированному отчёту, чтобы выводить в UI таски
- `pass_reports_to_teamcity` - отправлять ли файл с командами на teamcity (работает только при запуске из `teamcity`)
- `pass_logs_to_teamcity` - отправлять ли папку `log{*}` с логами `sandbox` на `teamcity` (работает только при запуске из `teamcity`)
- `retry_count` - количество перезапусков дочерних тасок, если они упали
- `time_to_kill` - таймаут выполнения таски, а то может и бесконечно выполняться, если что
- `api_levels` - версии АПИ, на которых прогонять тесты
- `delegate_emulator_launch` - указать, если мы сами, через плагин, запускаем эмуляторы

Как завести всё это:
1. завести `Owner`. [Добавить в members](https://sandbox.yandex-team.ru/admin/groups) сотрудников и робота
2. создать `ssh`-ключ роботу, если его нет (закинуть ключ в [секретницу сендбокса](https://sandbox.yandex-team.ru/admin/vault))
3. получить токен до секретницы для робота и положить его в [секретницу сендбокса](https://sandbox.yandex-team.ru/admin/vault)
4. скопировать нашу [таску](https://sandbox.yandex-team.ru/task/589609987/view) по запуску тестов: (сделать это можно под своим аккаунтом, можно под роботом, если есть доступ)
- указать свою репу, ветку, хеш-коммита
- указать ссылки на свои секреты, `ssh`-ключ
- поменять таску на запуск тестов (`connectedBaseDebugAndroidTest` у нас была раньше)
- убрать таски на отчёты, так как их пока нет
- поставить пока одну платформу
5. если таска прошла, то можно настраивать на TC
6. [конфигурация](https://teamcity.yandex-team.ru/admin/editBuild.html?id=buildType:Mobile_BlueMarketAndroid_Autotest) тестов на TC в Маркете
7. указать конечно же свои данные и:
- артефакты пока не нужны, появятся с отчётами
- добавляем `Build step` "Sandbox resources loader"
- добавляем `Build Feature` "Sandbox Task Launcher" с параметрами:
    - `task_type` - `GENERIC_TESTS_ANDROID_LAUNCHER`
    - `token` - токен робота [на сендбоксе](https://sandbox.yandex-team.ru/oauth)
    - `parameters_prefix` - `sandbox`. (с этим префиксом мы будем добавлять остальные параметры во вкладку с параметрами)
    - `owner` - ваш владелец из шага 1
    - `threshold` - 100
    - `task_priority` - `BACKGROUND:HIGH` (скорее всего повлияет на квоту, если выставить выше)
- добавляем параметры, которые выставляли в таске, когда запускали через сендбокс (можно подсмотреть у нас)
8. Вуаля, запускаем сборку
