Представьте себе мир, где каждый раз, когда вы вносите изменения в код вашего приложения, вы уверены, что ничего не сломалось. Где ошибки обнаруживаются еще до того, как пользователи успеют их заметить. Где ваш код не только работает, но и документируется автоматически, улучшая архитектуру проекта с каждым тестом. Звучит как мечта? На самом деле это реальность, если вы правильно используете тесты. В этой статье мы погрузимся в мир тестирования Android-приложений с использованием Jetpack Compose, рассмотрим различные виды тестов и научимся настраивать и писать инструментальные тесты для ваших Compose функций.
Зачем нужны вообще тесты?
-
Обеспечение качества кода
-
Проверять крайние случаи, которые может не учесть разработчик.
-
Тесты для регрессии
-
Само документация кода
-
Улучшение архитектуры кода
Основные виды тестов
Unit тесты
Mockito чтобы делать моки – объекты реальных классов с измененным поведением
Robolectric – нам нужен в случае, когда мы хотим протестировать код, который зависит от компонентов андроид или связан с контекстом.
Интеграционные тесты
Тестируем как работают разные компоненты системы друг с другом. Например база данных с приложением.
Инструментальные тесты
Можем протестировать уже сам ui. Например. Проверить что при нажатии на кнопку отобразится текст.
Screenshot тесты
Верифицирует совпадение скринов и кода.
Инструментальные тесты
Я предлагаю для наших compose функций использовать инструментальные тесты.
Они включены в основой фреймворк compose. Подключаются через gradle.
toml file: compose-bom = "2024.08.00" compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } compose-ui = { group = "androidx.compose.ui", name = "ui" } compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } Gradle app: implementation(platform(libs.compose.bom)) implementation(libs.compose.ui) androidTestImplementation(libs.compose.ui.test.junit4)
Практика
После того как они подключатся, будет автоматически создана папка androidTest. Внутри нее и будут создаваться наши тесты.
Я решил написать простенький тест для одного из экранов своего приложения. На нем можно ввести данные своей карты.
Что значат эти строки?
@RunWith(AndroidJUnit4::class) class AddPaymentInstrumentedTest { @get:Rule val composeTestRule = createComposeRule() private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext @Test fun testAddPaymentScreen() { composeTestRule.setContent { //тут будут наши тесты } } }
Если вкратце, то с помощью них мы создаем среду, в которой мы будем вызывать наши compose функции. Правилом может быть набор функция before или after. Но его удобнее внедрять в тесты и использовать.
Если подробно:
AndroidJUnit4 – Это аннотация, которая указывает, что тесты в данном классе должны выполняться с использованием AndroidJUnit4 тестового раннера.
createComposeRule — это функция, которая создает правило для тестирования Jetpack Compose UI компонентов.
Правило ComposeTestRule, созданное с помощью createComposeRule, предоставляет набор методов для тестирования Compose UI. Оно позволяет устанавливать содержимое Compose, взаимодействовать с UI-элементами и проверять их состояние.
И зачастую мы хотим ограничить ввод символов в поля карты.
Например в поле номер карты мы хотим вводить только 16 символов, при этом они могут быть только цифрами.
Давайте напишем для этого тест.
Сначала нам нужно найти ноду – так называется во фркймворке тестрования элемент в дереве ui.
Детали реализации
Поиск можно делать либо по тегу, либо по тексту. Для удобства, тег можно добавить в поле modifier. Он так и называется testTag(). Не путать с тэгом, который у нас был в XML. Этот можно использовать лишь для тестов. А после этого мы можем проверить, какая информация находится в поле. В этом нам помогает система matcher-ов и assertion-ов. Это классы, в которые можно передать необходимое условие и проверить, удовлетворяется ли оно. Например assertTestEquals(). Или assertIsDispayed(). Матчеров достаточно много, поэтому я прикрепил ссылку, чтобы не запутаться.
assertTestEquals
Что касается матчера assertTestEquals() – он проверяет идентичность текста. Причем как hint-а, так и введенного текста. Для этого мы передаем параметры hint и edit text. Это может поначалу вызвать недоумение, но так это работает.
Какие кейсы тестируем
Итак мы написали тесты для всех наших кейсов:
Задаем стейт с которым создается функция
val cardNumber = "1234" val hint = context.getString(R.string.card_number_label) var state by mutableStateOf( AddPaymentState(cardNumber = cardNumber) ) composeTestRule.setContent { AddPaymentScreen({ when (it) { is AddPaymentAction.CardNumberEntered -> { state = state.copy(cardNumber = it.cardNumber) } else -> {} } }, state) }
Начальное состояние поля
//check current cardNumber state onNodeWithTag(CARD_NUMBER_TEST_TAG) .assertIsDisplayed() onNodeWithTag(CARD_NUMBER_TEST_TAG) .assertTextEquals(hint, cardNumber, includeEditableText = true)
Состояние после ввода НЕ цифр
//strings are not allowed onNodeWithTag(CARD_NUMBER_TEST_TAG).performTextInput("test") onNodeWithTag(CARD_NUMBER_TEST_TAG) .assertTextEquals(hint, cardNumber, includeEditableText = true)
Состояние после ввода цифр
//digits are allowed val digitInput = "567855657787" val digitWithSpacesInput = "5678 5565 7787" onNodeWithTag(CARD_NUMBER_TEST_TAG).performTextInput(digitInput) onNodeWithTag(CARD_NUMBER_TEST_TAG) .assertTextEquals(hint, "$cardNumber $digitWithSpacesInput", includeEditableText = true)
Состояние после ввода больше чем ограничение на макс кол-во символов
//no more then 16 digits allowed val moreInput = "5678" onNodeWithTag(CARD_NUMBER_TEST_TAG).performTextInput(moreInput) onNodeWithTag(CARD_NUMBER_TEST_TAG) .assertTextEquals(hint, "$cardNumber $digitWithSpacesInput", includeEditableText = true)
Для удобства, разные тесты можно разбить на отдельные функции, тогда они не будут завершаться после падения одного.
Как выглядит ошибка в тестах
java.lang.AssertionError: Failed to assert the following: (Text + EditableText = [Номер карты,12341]) Semantics of the node: Node #14 at (l=44.0, t=242.0, r=1036.0, b=462.0)px, Tag: 'cardNumber' EditableText = '1234 1' TextSelectionRange = 'TextRange(0, 0)' ImeAction = 'Default' Focused = 'false' Text = '[Номер карты]' Actions = [GetTextLayoutResult, SetText, InsertTextAtCursor, SetSelection, PerformImeAction, OnClick, OnLongClick, PasteText, RequestFocus, SetTextSubstitution, ShowTextSubstitution, ClearTextSubstitution] MergeDescendants = 'true' Has 7 siblings Selector used: (TestTag = 'cardNumber')
Как видим, дается достаточно подробное описание того, где свалился тест, и что пошло не так. А также самой ноды, которая сломала тесты.
Что еще можно протестировать? Частые случаи
-
Нажатие на кнопку — можем как имитировать нажатие, так и проверять, было ли оно произведено
-
Enabled/disabled состояние. Например состояние кнопки
-
Visibility — видимость элемента
-
и многое другое
Итог
Вот и все. В итоге мы научились запускать инструментальные тесты для compose функций. Остался добавить что это достаточно трудоемкая операция. Запускается на девайсе. И запускать их при каждой ci сборке может быть затратно. Поэтому можно настроить сервис, который будет запускать такие тесты, например, раз в день – ночью. Например облачный сервис типа AWS. Ну и локально, если что-то пошло не так.
ссылка на оригинал статьи https://habr.com/ru/articles/850588/
Добавить комментарий