Разработка
Сквозное тестирование с помощью шаблона Робот и Jetpack Compose
Я поделюсь с вами шаблоном, чтобы сэкономить ваше время, и расскажу, как расширять его с помощью других роботов.
В этой статье я хочу показать вам, как систематизировать написание сквозных (end-to-end) тестов с помощью шаблона робот. Я поделюсь с вами шаблоном, чтобы сэкономить ваше время, и расскажу, как расширять его с помощью других роботов. В конце я хочу поделиться некоторыми советами и выводами после внедрения этого шаблона.
Сквозные тесты являются важной частью стратегии тестирования. Они имитируют поведение пользователя и проходят через все приложение, чтобы проверить, правильно ли оно работает. Несмотря на то, что тесты требуют больших затрат времени и средств на обслуживание, они могут помочь вам узнать о непреднамеренных изменениях.
Что такое шаблон робот?
Если вы пробовали возиться с тестированием Jetpack Compose, вы в той или иной степени знакомы с правилом ComposeTestRule
и его методами. Вот небольшой фрагмент кода, показывающий, как это выглядит:
class ComposeTest { @get:Rule val composeTestRule = createComposeRule() @Test fun testMyComposable() { // start the app launchApp<MainActivity>() // assert main screen title composeTestRule.onNode(hasText("Main screen")).assertIsDisplayed() // find some image with subtext composeTestRule.onNode(hasContentDescription("Main image").and(hasAnySibling(hasText("Subtitle of the page")))).assertIsDisplayed() // go ahead to another screen composeTestRule.onNode(hasTextExactly("Next").and(hasClickAction())).performClick() // wait for another screen composeTestRule.waitUntilExactlyOneExists(hasText("Second screen")) // check another image, if it is visible composeTestRule.onNode(hasContentDescription("Second image")).assertIsDisplayed() } }
Но методы составления правил могут быстро стать повторяющимися и нечитаемыми при тестировании. Каждый экран или поток пользователя нуждается в событиях нажатия, на каждом экране мы хотим проверить наличие текста, проверить элемент списка, проверить перечеркнутую галочку и т.д.
Используя паттерн робот, вы создаете «робота» для каждого экрана или пользовательского потока вашего приложения, который обладает методами, имитирующими поведение пользователя. Впоследствии вы можете смешивать и объединять их в несколько тестов с разными целями тестирования. Робот знает, как нажать кнопку «Войти», проверить, присутствует ли первый пункт списка ToDo и т.д.
А вот чего мы хотим добиться:
class ComposeTest { @get:Rule val composeTestRule = createComposeRule() @Test fun testMyComposable() { launchApp<MainActivity>() // call robot to operate the screen with(FirstScreenRobot(composeTestRule)) { assertMainContent() clickNext() } // call another robot to operate following screen with(SecondScreenRobot(composeTestRule)) { assertSecondScreen() } } }
Роботы скрывают логику, которую мы использовали для экрана.
class FirstScreenRobot(val composeTestRule: ComposeTestRule) { fun assertMainContent() { composeTestRule.onNode(hasText("Main screen")).assertIsDisplayed() composeTestRule.onNode(hasContentDescription("Main image").and(hasAnySibling(hasText("Subtitle of the page")))).assertIsDisplayed() } fun clickNext() = composeTestRule.onNode(hasTextExactly("Next").and(hasClickAction())).performClick() } class SecondScreenRobot(composeTestRule) { fun assertSecondScreen() { composeTestRule.waitUntilExactlyOneExists(hasText("Second screen")) composeTestRule.onNode(hasContentDescription("Second image")).assertIsDisplayed() } }
Преимущества паттерна робот
- возможность повторного использования роботов в других тестах
- повышенная читабельность теста — он более выразителен
- модульность каждого робота — он специализируется на тестировании одного экрана/потока
- уменьшение побочных эффектов, так как изменения в одном роботе влияют только на этот один робот.
Пример использования шаблона робот
Методы повторяются во всех роботах. Чтобы избежать этого, мы хотим абстрагировать как можно больше методов. Вот одна из обобщенных версий, унаследованная другими роботами и используемая в одном из моих проектов.
По моему опыту, взаимодействие с текстами, изображениями и кнопками может быть абстрагировано в вызовах, аналогичных этому.
abstract class Robot(val composeRule: ComposeTestRule) { // assertion of buttons and clicking them fun clickTextButton(text: String) = composeRule.onNode(hasTextExactly(text)).performClick() fun clickIconButton(description: String) = composeRule.onNode( hasContentDescription(description).and( hasClickAction() ) ).performClick() fun goBack() = clickIconButton("Back button") // uses the same description in all app fun assertIconButton(description: String) = composeRule.onNode(hasContentDescription(description).and(hasClickAction())).assertExists() fun assertTextButton(text: String) = composeRule.onNode(hasText(text).and(hasClickAction())).assertExists() fun assertTextButtonWithIcon(text: String, description: String) = composeRule.onNode( hasText(text).and(hasClickAction()).and( hasAnySibling(hasClickAction().and(hasContentDescription(description))) ).assertExists() ) fun assertImage(description: String) = composeRule.onNode(hasContentDescription(description)).assertExists() // text assertions fun assertText(text: String, ignoreCase: Boolean = false, substring: Boolean = false) = composeRule.onNode(hasText(text, ignoreCase = ignoreCase, substring = substring)) .assertExists() fun assertDoesNotExistText( text: String, ignoreCase: Boolean = false, substring: Boolean = false ) = composeRule.onNode(hasText(text, ignoreCase = ignoreCase, substring = substring)) .assertDoesNotExist() fun assertTextBesideImage(text: String, description: String) { composeRule.onNode( hasText(text).and( hasAnySibling(hasContentDescription(description)) ) ).assertExists() } @OptIn(ExperimentalTestApi::class) fun waitFor(matcher: SemanticsMatcher) = composeRule.waitUntilExactlyOneExists(matcher) }
Конечно, у каждого приложения свои потребности, поэтому не стесняйтесь адаптировать шаблон под нужды проекта. Или просто вдохновитесь им и создайте свою собственную версию.
После этого обобщенный робот может быть унаследован любым другим роботом и повторно использовать его методы. Например:
class ExampleRobot(composeRule: ComposeTestRule): Robot(composeRule) { fun checkScreen() { waitFor(hasContentDescription("Example screen main icon").and(hasNoClickAction())) assertImage("Example screen main icon") assertText("This is example screen of tutorial.", substring = true) assertTextButton("Next") } fun clickNext() = clickTextButton("Next") }
ExampleRobot может быть использован в ваших UI-тестах следующим образом:
with(ExampleRobot(composeRule)) { checkScreen() clickNext() }
Не пытайтесь напичкать шаблонного робота всеми функциями, которые попадутся вам на глаза. Методы, специфичные для экрана, следует держать в роботах, специфичных для экрана, и нигде больше. Шаблон должен облегчить вам жизнь с помощью вызовов, которые вы постоянно делаете в приложении.
Реальный пример
Недавно я создавал себе приложение для Bluetooth с открытым исходным кодом, которое предупреждает, если мой Bluetooth включен, а подключенного устройства нет. Для примера я использую главный экран приложения. Он просто показывает, включен или выключен фоновый воркер — это просто визуализация одной кнопки.
Самое замечательное в тестировании пользовательского интерфейса то, что вам не нужно знать, как все работает под капотом, потому что вы смотрите на приложение с точки зрения пользователя.
То же самое касается и написания тестов, потому что мы хотим наблюдать за приложением и тем, как оно работает, а не возиться с ним.
Для создания тестов для главного экрана нам нужно знать только то, что анимация на изображении содержит семантику stateDescription, которая меняется в зависимости от того, запущен сервис или нет.
Мы хотим создать следующие тесты:
- проверить, выключена ли задача по умолчанию
- попытаться запустить задачу и проверить изменения в пользовательском интерфейсе
- попробовать включить и выключить задачу
Создание робота для главного экрана
class MainRobot(composeRule: ComposeTestRule) : Robot(composeRule) { fun checkIdling() { checkAnimationOff() assertText("Optimise Bluetooth usage") assertTextButton("Turn on") } fun checkWorking() { checkAnimationOn() assertText("Checking Bluetooth perpetually") assertTextButton("Turn off") } fun checkMainScreen() = composeRule.onNode( hasStateDescription("Turned off").or( hasStateDescription("Turned on") ).or( hasStateDescription("Waiting to resolve issue") ) ).assertExists() fun clickTurnOnButton() = clickTextButton("Turn on") fun clickTurnOffButton() = clickTextButton("Turn off") private fun checkAnimationOff() = composeRule.onNode( hasStateDescription("Turned off") ).assertExists() private fun checkAnimationOn() = composeRule.onNode( hasStateDescription("Turned on")
Таким образом робот охватывает все основные взаимодействия с главным экраном:
- нажатие на кнопку
- проверка текущего состояния экрана.
Тесты главного экрана
@LargeTest @RunWith(AndroidJUnit4::class) class MainScreenTest { @get:Rule val composeTestRule = createEmptyComposeRule() @Test fun checkIfJobIsOff() { launchApp<MainActivity>() MainRobot(composeTestRule).checkIdling() } @Test fun checkIfJobIsOff_TurnItOn() { launchApp<MainActivity>() with(MainRobot(composeTestRule)) { checkIdling() clickTurnOnButton() checkWorking() } } @Test fun checkIfJobIsOff_TurnItOnAndOff() { launchApp<MainActivity>() with(MainRobot(composeTestRule)) { checkIdling() clickTurnOnButton() checkWorking() clickTurnOffButton() checkIdling() } } }
Как видите, финальные тесты содержат только базовую настройку с правилом композитного теста и запуск самого приложения. После этого мы передаем управление реальному роботу, и он самостоятельно управляет приложением. Тесты стали гораздо более выразительными и простыми для понимания даже с точки зрения кода.
Более того, теперь этого робота можно объединить с другими в одном тесте.
Например:
@Test fun navigateThroughAppDrawerAllScreens() { launchApp<MainActivity>() with(NavigationRobot(composeTestRule)) { MainRobot(composeTestRule).checkIdling() openSettingsScreenViaDrawer() MainSettingsRobot(composeTestRule).checkScreenContentWithoutAdvancedTracking() openAboutScreenViaDrawer() AboutRobot(composeTestRule).checkAboutScreen() openMainScreenViaDrawer() MainRobot(composeTestRule).checkMainScreen() } }
Заключение
Если вы создадите такого робота для каждого экрана, вы сможете с легкостью создавать тесты пользовательского интерфейса и тестировать практически все. Возьмите шаблон, настройте его по своему усмотрению, чтобы облегчить себе жизнь и поставлять в магазин приложения более высокого качества.
Примеры взяты из моего хобби-проекта с открытым исходным кодом под названием BluModify — не стесняйтесь поработать с ним через GitHub.