Статьи
Прекратите использовать тестовые теги в Jetpack Compose
Использование семантики делает ваше приложение доступным для людей с особыми потребностями и в то же время сохраняет его тестируемость.
Тестирование UI с помощью Jetpack Compose в основном основано на поиске узлов в дереве семантики, которое состоит из composable элементов.
Я заметил, что некоторые люди используют тестовые теги (test tags) в качестве модификаторов для своих составных элементов, чтобы сделать их легкодоступными.
Вот небольшой пример:
fun PrimaryButton(
modifier: Modifier = Modifier,
text: Int, onClick: OnClickFunction) {
Button(
// tag in the primary button
modifier = modifier.testTag("Primary button"),
onClick = onClick,
) {
Text(
stringResource(id = text),
)
}
}
Для быстрой разработки и первых запусков тестов это приемлемый путь, чтобы убедиться, что все протестировано. Тестовые метки:
- просты в реализации
- дают возможность быстрого тестирования
- отсутствие двусмысленности
Пока все хорошо. Однако у этого есть огромный минус.
Он загрязняет ваш продакшн код тегами, которые предназначены для тестирования. Производственный код не должен содержать ничего, относящегося к тестированию, по крайней мере, максимально стремиться к этому.
Другие минусы:
- необходимо поддерживать их организованность
- теги легко забываются разработчиком, если код подвергается рефакторингу
- в связи с этим необходимо оценивать сбои в тестах, что может привести к потере времени
Есть лучший способ — дополнительная семантика
Тестовые теги не предоставляют ничего, кроме способа поиска элементов пользовательского интерфейса во время тестирования пользовательского интерфейса. Семантика используется в Android для того, чтобы предоставить пользователям с особыми потребностями различные способы взаимодействия с вашим приложением. Например, с помощью TalkBack, чтения с экрана или Switch Access, который итеративно просматривает ваш пользовательский интерфейс, а пользователь выбирает элемент с кликабельным аксессуаром.
Вам следует придерживаться общепринятых способов тестирования. Следующие варианты показывают дополнительные способы поиска узлов в семантическом дереве.
Описание содержимого
Это общеизвестный способ, так как все будут его использовать. Каждое изображение или иконка в Jetpack Compose просят вас заполнить параметр contentDescription. Этот параметр можно заполнить каким-то осмысленным описанием и в то же время использовать его в тесте. Если изображение не несет никакой смысловой нагрузки и является просто косметическим, вы можете передать null, и оно будет проигнорировано в рамках семантики. Реализация:
xxxxxxxxxx
Image(
painter = painterResource(R.drawable.important_image),
// here you pass your description or null, if it is not important
contentDescription = stringResource(id = R.string.important_image_description),
)
В тесте:
xxxxxxxxxx
composeRule.onNode(hasContentDescription("Description of the image")).assertExists()
Описание действия для клика
В большинстве случаев вы сможете найти пользовательскую кнопку по описанию или тексту. Но есть один дополнительный слой, который может быть полезен при тестировании и одновременном обеспечении доступности вашего приложения. Вы можете использовать clickable
или semantics
поле onClick с дополнительными clickLabel
и clickAction
. Метка информирует пользователя о действии, которое произойдет после нажатия на компонент.
xxxxxxxxxx
// clickable modifier
Column(
Modifier.clickable(
onClickLabel = stringResource(R.string.on_button_click_label),
onClick = {}
)
) {}
// semantics modifier
Column(
Modifier.semantics(
onClick(
label = stringResource(R.string.on_button_click_label),
action = {return true}
)
)
) {}
Семантика версии ожидает возврата булевого значения. Она должна знать, обрабатывается ли действие.
Для кликабельного матчера не существует готового семантического матчера, но мы можем его создать.
xxxxxxxxxx
// matcher based on the click label
fun hasClickLabel(label: String) = SemanticsMatcher("Clickable action with label: $label") {
it.config.getOrNull(
SemanticsActions.OnClick
)?.label == label
}
В тесте:
xxxxxxxxxx
composeRule.onNode(hasClickLabel("Moves to next screen")).assertExists()
Я хочу, чтобы вы ознакомились с другими действиями класса SemanticsActions, которые тоже можно использовать.
Описание состояния
Большинство приложений имеют определенное состояние, чтобы показать пользователю конкретный контент. Вы можете использовать stateDescription
для описания текущего состояния экрана, кнопки или любого другого представления. Реализация довольно проста. Основываясь на механизме обработки состояния, вы можете изменить stateDescription
в соответствии с вашими требованиями. Затем состояние интерпретируется для пользователя.
xxxxxxxxxx
fun MainScreen(state: State, onClick: () -> Unit) {
val turnedOffDescription = stringResource(id = R.string.turned_off_state_description)
val turnedOnDescription = stringResource(id = R.string.turned_on_state_description)
val waitingDescription = stringResource(id = R.string.waiting_state_description)
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize()) {
CustomComposable(
modifier = Modifier
.semantics {
// definistion of state description based on state
stateDescription = when (state) {
State.TurnedOff -> turnedOffDescription
State.TurnedOn -> turnedOnDescription
State.WaitingForAction -> waitingDescription
}
},
state = state
)
/**
* other composables
*/
}
}
В тесте:
xxxxxxxxxx
composeRule.onNode(hasStateDescription("Description of state")).assertExists()
Роль composable
Сервисы доступности используют для описания role
элемента composable. Все распространенные составные элементы, такие как кнопки, переключатели, флажки и другие, имеют эту роль изначально. Однако все композитные элементы входят в состав других композитных элементов, таких как строки с текстом, например. Желательно сделать весь элемент кликабельным, чтобы он содержал текущее значение. Вот пример с кнопкой переключения:
xxxxxxxxxx
var currentValue by remember { mutableStateOf(false) }
Row(
Modifier
.toggleable(
value = currentValue,
role = Role.Switch,
onValueChange = { currentValue = !currentValue }
)
.padding(8.dp)
.fillMaxWidth()
) {
Text("Setting description", Modifier.weight(1f))
Switch(checked = currentValue, onCheckedChange = null)
}
Передача null в переключатель делает его отключенным. Состояние контролируется модификатором toggleable
в строке.
В тесте мы можем использовать роль следующим образом:
xxxxxxxxxx
fun hasRole(role: Role) = SemanticsMatcher("Searches for role: $role") {
it.config.getOrNull(SemanticsProperties.Role) == role
}
// in test
composeRule.onNode(
hasRole(Role.Switch).and(
hasText("Setting description")
)).assertExists()
Последовательная семантика
Если у вас есть сложная composable, которая ведет себя как единое целое, она может быть составной для размещения многокатегорийных данных, таких как события, заголовки и т.д. Вы можете опустить семантику всех дочерних композитов и объявить ее в родительском композите один раз.
Не создавайте семантику в нескольких составных элементах, если она не нужна:
xxxxxxxxxx
val actionDescription = stringResource(id = R.string.actionDescription)
val contentDescription = stringResource(id = R.string.imageDescription)
Row {
Image(
painter = painterResource(R.drawable.important_image),
contentDescription = contentDescription,
),
PrimaryButton(
modifier = Modifier.clickable(
onClickLabel = clickActionLabel,
onClick = {}
)
)
}
Создайте единую семантическую декларацию поверх всего независимого элемента:
xxxxxxxxxx
val actionDescription = stringResource(id = R.string.actionDescription)
val contentDescription = stringResource(id = R.string.imageDescription)
Row(
// row is parent of the composable - it owns the semantics
modifier = Modifier.semantics {
customActions = listOf(
CustomAccessibilityAction(
label = actionDescription,
// custom function
{ return true}
),
)
contentDescription = contentDescription
}
) {
Image(
painter = painterResource(R.drawable.important_image),
contentDescription = null
),
PrimaryButton(
// to make sure, no semantics are addeed
modifier = Modifier.clearAndSetSemantics { }
)
}
Это позволит сохранить семантику явной и чистой в коде. Описание содержимого и метка действия могут быть найдены с помощью SemanticMatcher, как указано выше.
Чтобы сделать ваши тесты еще лучше, более подробными и чистыми, вы можете ознакомиться с моей другой статьей о шаблоне Робот в Jetpack Compose.
Заключение
Использование семантики делает ваше приложение доступным для людей с особыми потребностями и в то же время сохраняет его тестируемость. К сожалению, бывают ситуации, когда тестовый тег неизбежен или просто не стоит тратить время на семантику. С другой стороны, я надеюсь, что это сделает внедрение семантик менее сложным для вас и упростит чью-то жизнь с помощью вашего приложения.
Подробнее о семантике и доступности вы можете узнать на официальном сайте.
-
Программирование3 недели назад
Конец программирования в том виде, в котором мы его знаем
-
Видео и подкасты для разработчиков6 дней назад
Как устроена мобильная архитектура. Интервью с тех. лидером юнита «Mobile Architecture» из AvitoTech
-
Магазины приложений3 недели назад
Магазин игр Aptoide запустился на iOS в Европе
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2025.8