Site icon AppTractor

Сквозное тестирование с помощью шаблона Робот и 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.

Источник

Exit mobile version