Паттерн Page Object появился в тестировании web и очень хорошо себя там зарекомендовал. Когда я начал автоматизировать тесты для android приложения, то первым делом подумал про него. Поискал информацию в сети, поспрашивал коллег и, в принципе, не нашел доводов не попробовать. Предлагаю посмотреть, что из этого вышло.
Классический Page Object подразумевает два уровня абстракции: элементы страницы и тесты. Я выделяю еще один — бизнес-логика. Отмечу, что то, как вы построите ваш фреймворк, будет очень сильно влиять на простоту написания тестов в будущем, а так же на их поддержку. Я стараюсь делать так, чтобы код теста выглядел как будто это обычный тест-кейс, написанный рядовым тестером. Т.е. начинаю с конца:
- Пишу красивый и понятный код тест-кейса,
- Реализую методы из тест-кейса в слое бизнес-логики,
- Описываю элементы, которые нужны для работы теста.
Данный подход хорош тем, что мы не делаем ничего лишнего — фреймворк построен на столько, на сколько это нужно для работы тестов. Можно сказать, что это концепция 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
и наслаждаемся как «вкалывают роботы, а не человек» (с).