Pull to refresh

Автоматизируем UI-тесты Android приложения с использование паттерна Page Object

Reading time 6 min
Views 14K
image

Паттерн Page Object появился в тестировании web и очень хорошо себя там зарекомендовал. Когда я начал автоматизировать тесты для android приложения, то первым делом подумал про него. Поискал информацию в сети, поспрашивал коллег и, в принципе, не нашел доводов не попробовать. Предлагаю посмотреть, что из этого вышло.

Классический Page Object подразумевает два уровня абстракции: элементы страницы и тесты. Я выделяю еще один — бизнес-логика. Отмечу, что то, как вы построите ваш фреймворк, будет очень сильно влиять на простоту написания тестов в будущем, а так же на их поддержку. Я стараюсь делать так, чтобы код теста выглядел как будто это обычный тест-кейс, написанный рядовым тестером. Т.е. начинаю с конца:

  1. Пишу красивый и понятный код тест-кейса,
  2. Реализую методы из тест-кейса в слое бизнес-логики,
  3. Описываю элементы, которые нужны для работы теста.

Данный подход хорош тем, что мы не делаем ничего лишнего — фреймворк построен на столько, на сколько это нужно для работы тестов. Можно сказать, что это концепция MVP в тестировании: быстро сделали кусочек, и он уже начал приносить пользу. Если вы сначала пишете тысячи строк, описывая страницы вашего приложения и способы взаимодействия с ними, а месяца через три вылезаете из «норы» для первого клика и осознаете, что надо было сделать все по-другому, то большая часть вашего творения будет обречена навсегда «пылиться» в подвалах git'а… Истинный тестировщик знает, что чем раньше нашел ошибку, тем дешевле ее исправлять. Используйте этот подход во всем — написал тест за пару часов, попробовал, не понравилось — выкинул, извлек урок, поехал дальше.

Итак, входные данные:

  • Android приложение для торговли на бирже;
  • Java, чтобы работать в одном стэке с разработчиками;
  • базовый фреймворк UI Automator;
  • нужно написать тест логина в приложение.

Подготовка проекта


Так как я старался максимально интегрироваться в процесс разработчиков, то новый проект создавать не стал. Согласно документации, инструментальные тесты необходимо разместить в папке src/androidTest/java. В моем случае сборщик уже был настроен, если у вас не так, то почитайте про конфигурацию билда. Также нам нужна среда разработки, Android SDK, Emulator, tools, platform-tools и необходимые платформы для эмулятора. Если вы пользуетесь Android Studio, то все это можно быстро поставить через SDK Manager:



Слой тестов


Как писал выше, будем автоматизировать тест на логин в приложении. Помним, что код должен выглядеть так же, как обычный тест-кейс:

Precondition: запустить приложение.
Step 1: осуществить логин под учетной записью myLogin/myPassword.
Step 2: проверить имя текущего пользователя.
Expected Result: текущий пользователь «Иванов Иван».

Небольшой дисклеймер: согласно лучшим практикам тест дизайна, в предусловии нужно создать/найти учетную запись. Я этот момент опустил для простоты примера.

Наш класс будет выглядеть так:

@RunWith(AndroidJUnit4.class)
public class LoginTests {

    private TestApplication myApp;

    @Before
    public void setUp() {
        myApp = new TestApplication();
    }

    @Test
    public void testLogin() {
        myApp.login("myLogin","myPassword");
        String currentUser = myApp.getCurrentUserName();
        assertEquals("Wrong name", "Иванов Иван", currentUser);
    }

    @After
    public void tearDown() {
        myApp.close();
    }
}

Бизнес-логика


В тесте используется класс TestApplication() и два его метода: login() и getCurrentUserName(). Плюс нужен конструктор класса (в нем запуск приложения) и метод close() Фронт работы ясен:

public class TestApplication  {

    private UiDevice device;
    private PageObject page;

    public TestApplication() {
    }

    public void login(String login, String password) {
    }

    public String getCurrentUserName() {
        return ""
    }

    public void close() {
    }

}

У экземпляра нашего класса будет две переменные:

  • device, принадлежащая классу android.support.test.uiautomator.UiDevice — через нее взаимодействуем с нашим устройством;
  • page, класс PageObject, который мы будем создавать в следующем разделе.

Начнем с конструктора. В нем создадим экземпляры наших переменных и запустим приложение:

public TestApplication() {
    // Connect to device
    device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
    device.pressHome();

    // Get launch intent
    String packageName = InstrumentationRegistry.getTargetContext()
            .getPackageName();
    Context context = InstrumentationRegistry.getContext();
    Intent intent = context.getPackageManager()
            .getLaunchIntentForPackage(packageName)
            .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

    // Stat application
    context.startActivity(intent);

    // Get page objects
    page = new PageObject(device, packageName);
}

Пара советов про запуск приложения
Для бОльшего контроля тестируемого приложения можно добавить ожидания запуска лончера (если запускаете эмулятор «на холодную») и самого приложения. Так советуют и в документации.

Замечание: запуск приложения через android.content.Context.getTargetContext() подходит только в том случае, если ваши тесты в одном проекте с самим приложением. Если отдельно, то необходимо будет запускать через меню.

Бизнес-логика теста — это конкретные шаги, которые должен совершить пользователь для получения какого-либо значимого (для пользователя) результата. Осуществление входа под своей УЗ — значимый результат. Шаги: клик на кнопку «Login», ввести имя пользователя в поле «Login», ввести пароль в поле «Password», кликнуть на кнопку «Sign In». Таким образом, наш метод наполняется шагами:

public void login(String login, String password) {
    page.login().click();
    page.loginEntry().setText(login);
    page.passwordEntry().setText(password);
    page.signIn().click();
}

Для получения текущего пользователя все проще, просто получаем значение поля:

public String getCurrentUserName() {
    return page.currentUserName().getText();
}
}

А для закрытия приложения просто нажмем на кнопку Home:

public void close() {
    device.pressHome();
}

Описание элементов страницы


Концепция данного слоя — он должен возвращать готовые к использованию элементы (в нашем контексте это класс android.support.test.uiautomator.UiObject2). Т. е., потребитель не должен переживать на счет состояния объекта, если он вернулся, то с ним сразу же можно взаимодействовать: кликать, заполнять или считывать текст. Отсюда важное следствие — в этом слое будем реализовывать ожидания:

private UiObject2 getUiObject(BySelector selector) {
    return device.wait(Until.findObject(selector), 10000);
}

Метод, определенный выше, будет использоваться публичным интерфейсом нашего класса PageObject. Пример для поля «Login»:

public UiObject2 loginEntry() {
    return getUiObject(loginEntrySelector());
}

Осталось определить селектор, по которому будем находить поле. Экземпляр android.support.test.uiautomator.BySelector удобнее всего получить с помощью статических методов класса android.support.test.uiautomator.By. Я договорился с разработкой, что везде, по возможности, будут уникальные resource-id:

private BySelector loginEntrySelector() {
    return By.res(packageName, "login");
}

Инспекцию интерфейса удобно производить в утилите uiautomatorviewer, входящей в пакет tools (устанавливали в разделе Подготовка):



Выделяем нужный элемент и в разделе Node detail смотрим resource-id. Он состоит из названия пакета и, собственно, айдишника. Название пакета мы получили в конструкторе класса TestApplication, и использовали при создании объекта PageObject.

Код полностью:

класс PageObject
public class PageObject {

    private UiDevice device;
    private final String packageName;

    private BySelector loginButtonSelector() {
        return By.res(packageName, "user_view");
    }
    private BySelector loginEntrySelector() {
        return By.res(packageName, "login");
    }
    private BySelector passwordEntrySelector() {
        return By.res(packageName, "password");
    }
    private BySelector signInSelector() {
        return By.res(packageName, "btnLogin");
    }
    private BySelector userNameSelector() {
        return By.res(packageName, "user_name");
    }

    public PageObject(UiDevice device, String packageName) {
        this.device = device;
        this.packageName = packageName;
    }

    public UiObject2 login() {
        return getUiObject(loginButtonSelector());
    }
    public UiObject2 loginEntry() {
        return getUiObject(loginEntrySelector());
    }
    public UiObject2 passwordEntry() {
        return getUiObject(passwordEntrySelector());
    }
    public UiObject2 signIn() {
        return getUiObject(signInSelector());
    }
    public UiObject2 currentUserName() {
        return getUiObject(userNameSelector());
    }

    private UiObject2 getUiObject(BySelector selector) {
        return device.wait(Until.findObject(selector), 10000);
    }

}


Запуск тестов


На этом все, осталось запустить. Для начала подключаемся к устройству или стартуем эмулятор. Проверим подключение командой adb devices (утилита adb входит в состав platform-tools):

List of devices attached
emulator-5554   device

Если у вас gradle, то далее делаем

gradlew.bat connectedAndroidTest

и наслаждаемся как «вкалывают роботы, а не человек» (с).
Tags:
Hubs:
+9
Comments 23
Comments Comments 23

Articles