refactor project structure and stabilize execution
This commit is contained in:
@@ -1,33 +1,25 @@
|
|||||||
# OTUS Homework 7: Mobile Testing
|
# OTUS Homework 7: Mobile Testing
|
||||||
|
|
||||||
Проект содержит мобильные UI-тесты для приложения Wishlist на базе `selenide-appium`.
|
Проект содержит мобильные UI-тесты приложения Wishlist на `selenide-appium`.
|
||||||
|
|
||||||
Реализованы сценарии:
|
Сценарии:
|
||||||
- создание и редактирование списка желаний;
|
- создание/редактирование списка желаний;
|
||||||
- создание и редактирование подарка;
|
- создание/редактирование подарка;
|
||||||
- изменение статуса резервирования подарка другого пользователя.
|
- изменение статуса резервирования подарка другого пользователя.
|
||||||
|
|
||||||
## Архитектура
|
## Что реализовано по требованиям
|
||||||
- `Guice` для DI.
|
- `docker-compose` поднимает `wiremock` и 2 Android-эмулятора (`android-emulator-1`, `android-emulator-2`) для параллельного запуска.
|
||||||
- `JUnit 5 Extension` вместо базового тестового класса.
|
- APK хранится в `wiremock/__files/wishlist.apk` и устанавливается через Appium capability `app`.
|
||||||
- `AbsPageObject` -> `AbsBasePage` / `BaseMobileComponent`.
|
- DI на `Guice`, запуск через `JUnit 5 Extension`, балансировка эмуляторов через `BlockingQueue`.
|
||||||
- `BlockingQueue` для распределения тестов по эмуляторам.
|
- Подготовка тестовых данных выполняется через JDBC перед каждым тестом.
|
||||||
- код инфраструктуры и page object находится в `src/main/java`;
|
- Логи `logcat` сохраняются в `logcat.txt` через Appium logs API (без ADB-скриптов).
|
||||||
- в `src/test/java` находятся только тестовые классы.
|
|
||||||
|
|
||||||
## Инфраструктура
|
## Структура
|
||||||
`docker-compose.yml` поднимает:
|
- `src/main/java` — инфраструктура, конфиги, page/component object.
|
||||||
- `wiremock` для раздачи `wishlist.apk`;
|
- `src/test/java` — только тестовые классы.
|
||||||
- `android-emulator-1` (Android 13, Appium `:4723`, VNC `:6080`);
|
- `wiremock` — маппинги и APK.
|
||||||
- `android-emulator-2` (Android 12, Appium `:4725`, VNC `:6081`).
|
|
||||||
|
|
||||||
Приложение не маунтится в эмулятор и не ставится через ADB.
|
|
||||||
APK скачивается Appium по capability `app`.
|
|
||||||
Источник APK для Wiremock: файл `wishlist-349317-5fd795.apk` в корне проекта.
|
|
||||||
Эмулятор запускается без `privileged`, доступ к аппаратной виртуализации передается через `/dev/kvm`.
|
|
||||||
|
|
||||||
## Тестовые аккаунты
|
## Тестовые аккаунты
|
||||||
Тесты используют заранее созданные аккаунты:
|
|
||||||
- `user1us / user1us`
|
- `user1us / user1us`
|
||||||
- `user2us / user2us`
|
- `user2us / user2us`
|
||||||
- `user3us / user3us`
|
- `user3us / user3us`
|
||||||
@@ -35,43 +27,34 @@ APK скачивается Appium по capability `app`.
|
|||||||
|
|
||||||
`user4us` используется как владелец подарка в тесте резервирования.
|
`user4us` используется как владелец подарка в тесте резервирования.
|
||||||
|
|
||||||
## Подготовка
|
## Запуск
|
||||||
Нужно задать доступ к БД, иначе `mvn test` завершится ошибкой:
|
1. Поднять окружение:
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Убедиться, что сервисы `wiremock`, `android-emulator-1`, `android-emulator-2` имеют статус `healthy`:
|
||||||
|
```bash
|
||||||
|
docker compose ps
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Указать доступ к БД:
|
||||||
|
PowerShell:
|
||||||
```powershell
|
```powershell
|
||||||
$env:DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist"
|
$env:DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist"
|
||||||
$env:DB_USER="student"
|
$env:DB_USER="student"
|
||||||
$env:DB_PASSWORD="student"
|
$env:DB_PASSWORD="student"
|
||||||
```
|
```
|
||||||
|
bash:
|
||||||
## Запуск
|
|
||||||
1. Поднять окружение:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
export DB_URL="jdbc:postgresql://sql.otus.kartushin.su:5432/wishlist"
|
||||||
|
export DB_USER="student"
|
||||||
|
export DB_PASSWORD="student"
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Дождаться статуса `healthy` у `wiremock`, `android-emulator-1`, `android-emulator-2`:
|
4. Запустить тесты:
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
Тесты не ждут загрузку эмулятора сами. Готовность окружения проверяется на уровне Docker Compose.
|
|
||||||
|
|
||||||
3. Запустить тесты:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvn test
|
mvn test
|
||||||
```
|
```
|
||||||
|
|
||||||
По умолчанию тесты запускаются параллельно на уровне классов (2 потока), а сессии распределяются по эмуляторам через `BlockingQueue`.
|
Тесты запускаются параллельно по классам (2 потока) и распределяются по эмуляторам через очередь.
|
||||||
|
|
||||||
Опционально можно явно задать пул эмуляторов:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
$env:MOBILE_EMULATORS="android-emulator-1|http://localhost:4723|Android Emulator,android-emulator-2|http://localhost:4725|Android Emulator"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Логи
|
|
||||||
После выполнения тестов logcat сохраняется в файл `logcat.txt` в корне проекта через Selenium/Appium logs API.
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ services:
|
|||||||
image: wiremock/wiremock:3.9.1
|
image: wiremock/wiremock:3.9.1
|
||||||
volumes:
|
volumes:
|
||||||
- ./wiremock:/home/wiremock
|
- ./wiremock:/home/wiremock
|
||||||
- ./wishlist-349317-5fd795.apk:/home/wiremock/__files/wishlist.apk:ro
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/__admin/health | grep -q 'healthy'"]
|
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/__admin/health | grep -q 'healthy'"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
|
|||||||
@@ -13,13 +13,10 @@
|
|||||||
<maven.compiler.source>21</maven.compiler.source>
|
<maven.compiler.source>21</maven.compiler.source>
|
||||||
<maven.compiler.target>21</maven.compiler.target>
|
<maven.compiler.target>21</maven.compiler.target>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<selenide.version>7.3.1</selenide.version>
|
|
||||||
<selenide.appium.version>7.3.1</selenide.appium.version>
|
<selenide.appium.version>7.3.1</selenide.appium.version>
|
||||||
<junit.version>5.10.2</junit.version>
|
<junit.version>5.10.2</junit.version>
|
||||||
<slf4j.version>2.0.13</slf4j.version>
|
<slf4j.version>2.0.13</slf4j.version>
|
||||||
<logback.version>1.5.6</logback.version>
|
<logback.version>1.5.6</logback.version>
|
||||||
<selenium.version>4.20.0</selenium.version>
|
|
||||||
<allure.version>2.29.1</allure.version>
|
|
||||||
<guice.version>7.0.0</guice.version>
|
<guice.version>7.0.0</guice.version>
|
||||||
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
|
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
|
||||||
<surefire.version>3.2.5</surefire.version>
|
<surefire.version>3.2.5</surefire.version>
|
||||||
@@ -28,24 +25,7 @@
|
|||||||
<spotbugs.version>4.9.3</spotbugs.version>
|
<spotbugs.version>4.9.3</spotbugs.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencyManagement>
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
|
||||||
<groupId>org.seleniumhq.selenium</groupId>
|
|
||||||
<artifactId>selenium-bom</artifactId>
|
|
||||||
<version>${selenium.version}</version>
|
|
||||||
<type>pom</type>
|
|
||||||
<scope>import</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
</dependencyManagement>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.codeborne</groupId>
|
|
||||||
<artifactId>selenide</artifactId>
|
|
||||||
<version>${selenide.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.codeborne</groupId>
|
<groupId>com.codeborne</groupId>
|
||||||
<artifactId>selenide-appium</artifactId>
|
<artifactId>selenide-appium</artifactId>
|
||||||
@@ -63,16 +43,10 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter-engine</artifactId>
|
<artifactId>junit-jupiter</artifactId>
|
||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
|
||||||
<groupId>io.qameta.allure</groupId>
|
|
||||||
<artifactId>allure-junit5</artifactId>
|
|
||||||
<version>${allure.version}</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ public final class MobileConfig {
|
|||||||
}
|
}
|
||||||
String[] parts = trimmed.split("\\|");
|
String[] parts = trimmed.split("\\|");
|
||||||
String id = parts.length > 0 ? parts[0].trim() : "emulator-1";
|
String id = parts.length > 0 ? parts[0].trim() : "emulator-1";
|
||||||
String appiumUrl = parts.length > 1 ? parts[1].trim() : "http://localhost:4723";
|
String appiumUrl = parts.length > 1 ? parts[1].trim() : "http://127.0.0.1:4723";
|
||||||
String deviceName = parts.length > 2 ? parts[2].trim() : "Android Emulator";
|
String deviceName = parts.length > 2 ? parts[2].trim() : "Android Emulator";
|
||||||
String emulatorAppUrl = parts.length > 3 ? parts[3].trim() : defaultAppUrl;
|
String emulatorAppUrl = parts.length > 3 ? parts[3].trim() : defaultAppUrl;
|
||||||
emulators.add(new Emulator(id, appiumUrl, deviceName, emulatorAppUrl));
|
emulators.add(new Emulator(id, appiumUrl, deviceName, emulatorAppUrl));
|
||||||
}
|
}
|
||||||
if (emulators.isEmpty()) {
|
if (emulators.isEmpty()) {
|
||||||
emulators.add(new Emulator("emulator-1", "http://localhost:4723", "Android Emulator", defaultAppUrl));
|
emulators.add(new Emulator("emulator-1", "http://127.0.0.1:4723", "Android Emulator", defaultAppUrl));
|
||||||
}
|
}
|
||||||
return emulators;
|
return emulators;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,16 +4,10 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
public final class ProjectPaths {
|
public final class ProjectPaths {
|
||||||
private final Path projectRoot;
|
|
||||||
private final Path logcatFile;
|
private final Path logcatFile;
|
||||||
|
|
||||||
public ProjectPaths() {
|
public ProjectPaths() {
|
||||||
this.projectRoot = Paths.get("").toAbsolutePath().normalize();
|
this.logcatFile = Paths.get("").toAbsolutePath().normalize().resolve("logcat.txt");
|
||||||
this.logcatFile = projectRoot.resolve("logcat.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path projectRoot() {
|
|
||||||
return projectRoot;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Path logcatFile() {
|
public Path logcatFile() {
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ public final class MobileDriverFactory {
|
|||||||
.setPlatformName("Android")
|
.setPlatformName("Android")
|
||||||
.setAutomationName("UiAutomator2")
|
.setAutomationName("UiAutomator2")
|
||||||
.setDeviceName(emulator.deviceName())
|
.setDeviceName(emulator.deviceName())
|
||||||
.setApp(emulator.appUrl());
|
.setApp(emulator.appUrl())
|
||||||
|
.setSkipDeviceInitialization(true);
|
||||||
try {
|
try {
|
||||||
return new AndroidDriver(URI.create(emulator.appiumUrl()).toURL(), options);
|
return new AndroidDriver(URI.create(emulator.appiumUrl()).toURL(), options);
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ public final class CoreModule extends AbstractModule {
|
|||||||
String rawEmulators = value(
|
String rawEmulators = value(
|
||||||
"mobile.emulators",
|
"mobile.emulators",
|
||||||
"MOBILE_EMULATORS",
|
"MOBILE_EMULATORS",
|
||||||
"android-emulator-1|http://localhost:4723|Android Emulator,"
|
"android-emulator-1|http://127.0.0.1:4723|Android Emulator,"
|
||||||
+ "android-emulator-2|http://localhost:4725|Android Emulator"
|
+ "android-emulator-2|http://127.0.0.1:4725|Android Emulator"
|
||||||
);
|
);
|
||||||
List<MobileConfig.Emulator> emulators = MobileConfig.Emulator.parse(rawEmulators, appUrl);
|
List<MobileConfig.Emulator> emulators = MobileConfig.Emulator.parse(rawEmulators, appUrl);
|
||||||
return new MobileConfig(appPackage, appUrl, reservationOwner, emulators);
|
return new MobileConfig(appPackage, appUrl, reservationOwner, emulators);
|
||||||
|
|||||||
Reference in New Issue
Block a user