Всем привет
Не давно понадобилось автоматизировать одно приложение.Мне не хотелось использовать какие то посредники по типу Appium, во-первых, ресурсы были ограничены, на одном компьютере нужно было заставить работать 3-4 эмулятора, во-вторых с adb работать не так уж и трудно, в-третьих я наткнулся на библиотеку SharpAdbClient, в которой были уже реализованы базовые функции, но самых важных мне не хватало, поэтому пришлось дописать их самому.Я подумал почему бы не сделать библиотеку с расширенным функционалом, поэтому в этой статье будет обзор на библиотеку AdvancedSharpAdbClient.
Установка
Вы можете установить библиотеку через nuget, либо через package manager:
PM> Install-Package AdvancedSharpAdbClient
Так же проект доступен на github
Инициализация
Для работы библиотеки понадобится adb.exe.Я использовал эмулятор Nox,где adb идёт вместе с приложением и находится по пути Nox\bin\adb.exe.
Первым делом запустим сервер
using System;
using AdvancedSharpAdbClient;
namespace AdbTest
{
class Program
{
static void Main(string[] args)
{
if (!AdbServer.Instance.GetStatus().IsRunning)
{
AdbServer server = new AdbServer();
StartServerResult result = server.StartServer(@"F:\Nox\bin\adb.exe", false);
if (result != StartServerResult.Started)
{
Console.WriteLine("Can't start adb server");
return;
}
}
}
}
}
Далее нам необходимо создать новый клиент и подключиться к устройству
using System;
using System.Linq;
using AdvancedSharpAdbClient;
namespace AdbTest
{
class Program
{
static AdvancedAdbClient client;
static DeviceData device;
static void Main(string[] args)
{
if (!AdbServer.Instance.GetStatus().IsRunning)
{
AdbServer server = new AdbServer();
StartServerResult result = server.StartServer(@"F:\Nox\bin\adb.exe", false);
if (result != StartServerResult.Started)
{
Console.WriteLine("Can't start adb server");
return;
}
}
client = new AdvancedAdbClient();
client.Connect("127.0.0.1:62001"); // Ip Nox'а
device = client.GetDevices().FirstOrDefault(); // Выбираем девайс из подключенных
if (device == null)
{
Console.WriteLine("Can't connect to device");
return;
}
}
}
}
Тут я покажу вам лайфхак, как можно автоматически получать Ip эмуляторов.Это пригодится при работе с несколькими эмуляторами одновременно(многопоточность).
Рассмотрим на примере Nox'a, у которого каждый порт начинается с 620, например:
127.0.0.1:62001
127.0.0.1:62025
127.0.0.1:62040
Для поиска можно воспользоваться утилитой netstat, которая слушает активные порты и подключения
Получение IP для нескольких эмуляторов
static List<string> deviceports = new List<string>();
static string GetIP()
{
while (true)
{
Process[] processes = Process.GetProcessesByName("NoxVMHandle");
foreach (Process process in processes)
{
ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "/c netstat -a -n -o | find \"" + process.Id + "\" | find \"127.0.0.1\" | find \"620\"");
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = true;
var proc = new Process();
proc.StartInfo = startInfo;
proc.Start();
proc.WaitForExit();
MatchCollection matches = Regex.Matches(proc.StandardOutput.ReadToEnd(), "(?<=127.0.0.1:)62.*?(?= )");
foreach (Match match in matches)
{
if (match.Value != "" && !deviceports.Contains(match.Value))
{
deviceports.Add(match.Value);
return "127.0.0.1:" + match.Value;
}
}
}
}
}
Так же, если вы хотите работать в многопотоке, в каждом потоке необходимо создавать новый AdvancedAdbClient для стабильной работы.
Пример многопоточной работы
static ConcurrentQueue<string> deviceports = new ConcurrentQueue<string>(); // Используем потокобезопасный список
static object locker = new object();
static string GetIP()
{
while (true)
{
Process[] processes = Process.GetProcessesByName("NoxVMHandle");
foreach (Process process in processes)
{
ProcessStartInfo startInfo = new ProcessStartInfo("cmd", "/c netstat -a -n -o | find \"" + process.Id + "\" | find \"127.0.0.1\" | find \"620\"");
startInfo.UseShellExecute = false;
startInfo.CreateNoWindow = true;
startInfo.RedirectStandardOutput = true;
var proc = new Process();
proc.StartInfo = startInfo;
proc.Start();
proc.WaitForExit();
MatchCollection matches = Regex.Matches(proc.StandardOutput.ReadToEnd(), "(?<=127.0.0.1:)62.*?(?= )");
foreach (Match match in matches)
{
if (match.Value != "" && !deviceports.Contains(match.Value))
{
deviceports.Enqueue(match.Value);
return "127.0.0.1:" + match.Value;
}
}
}
}
}
static void Main(string[] args)
{
GetIP();
if (!AdbServer.Instance.GetStatus().IsRunning)
{
AdbServer server = new AdbServer();
StartServerResult result = server.StartServer(@"F:\Nox\bin\adb.exe", false);
if (result != StartServerResult.Started)
{
Console.WriteLine("Can't start adb server");
return;
}
}
// Перед запуском необходимо добавить в Multi Drive 3 эмулятора
for (int i = 0; i < 3; i++) // Запускаем три эмулятора
{
new Thread(() =>
{
Process process = new Process();
process.StartInfo.FileName = @"F:\Nox\bin\Nox.exe";
process.StartInfo.Arguments = $"-clone:Nox_{i}"; // Запускаем i-тый эмулятор из Multi-Drive
process.Start();
AdvancedAdbClient client = new AdvancedAdbClient();
lock (locker) // Во избежании ошибок
{
client.Connect(GetIP()); // Ip Nox'а
}
DeviceData device = client.GetDevices().FirstOrDefault();
if (device == null)
{
Console.WriteLine("Can't connect to device");
return;
}
}).Start();
}
}
Автоматизация
Поиск элемента
Для поиска элемента используется метод FindElement и FindElements
static AdvancedAdbClient client;
static DeviceData device;
static void Main(string[] args)
{
client = new AdvancedAdbClient();
client.Connect("127.0.0.1:62001");
device = client.GetDevices().FirstOrDefault();
Element el = client.FindElement(device, "//node[@text='Login']");
}
Можно указать время ожидания элемента (по умолчанию его нету)
Element el = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(5));
Element[] els = client.FindElements(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(5));
Получение атрибута элемента
Каждый элемент содержит свои аттрибуты, для получения необходимо обратиться к полю attributes
static void Main(string[] args)
{
...
Element el = client.FindElement(device, "//node[@resource-id='Login']", TimeSpan.FromSeconds(3));
string eltext = el.attributes["text"];
string bounds = el.attributes["bounds"];
...
}
Клик по элементу
Вы можете кликнуть по координатам на экране
static void Main(string[] args)
{
...
client.Click(device, 600, 600); // Click on the coordinates (600;600)
...
}
Либо же по найденному элементу
static void Main(string[] args)
{
...
Element el = client.FindElement(device, "//node[@text='Login']", TimeSpan.FromSeconds(3));
el.Click();// Click on element by xpath //node[@text='Login']
...
}
Свайп
AdvancedSharpAdbClient позволяет свайпнуть от одного элемента к другому
static void Main(string[] args)
{
...
Element first = client.FindElement(device, "//node[@text='Login']");
Element second = client.FindElement(device, "//node[@text='Password']");
client.Swipe(device, first, second, 100); // Swipe 100 ms
...
}
И так же по координатам
static void Main(string[] args)
{
...
device = client.GetDevices().FirstOrDefault();
client.Swipe(device, 600, 1000, 600, 500, 100); // Swipe from (600;1000) to (600;500) on 100 ms
...
}
Ввод текста
Текст может быть абсолютно любым, но учтите, что adb не поддерживает кириллицу.
static void Main(string[] args)
{
...
client.SendText(device, "text"); // Элемент должен быть в фокусе
...
}
Так же можно автоматически фокусироваться на элементе (кликать, а потом вводить текст)
static void Main(string[] args)
{
...
client.FindElement(device, "//node[@resource-id='Login']").SendText("text"); // Автоматически фокусируется
...
}
Очистка поля ввода
Для очистки поля ввода необходимо указать максимальное количество символов в поле
static void Main(string[] args)
{
...
client.ClearInput(device, 25); // The second argument is to specify the maximum number of characters to be erased
...
}
Автоматически определять длинну текста (может работать неправильно)
static void Main(string[] args)
{
...
client.FindElement(device, "//node[@resource-id='Login']").ClearInput(); // Get element text attribute and remove text length symbols
...
}
Отправка нажатия клавиши (keyevent)
Список эвентов вы можете посмотреть тут
static void Main(string[] args)
{
...
client.SendKeyEvent(device, "KEYCODE_TAB");
...
}
Команды устройства
Все команды устройства вы можете посмотреть на страничке Github, здесь я покажу лишь 2 главных метода.
Установка apk
static void Main(string[] args)
{
...
PackageManager manager = new PackageManager(client, device);
manager.InstallPackage(@"C:\Users\me\Documents\mypackage.apk", reinstall: false);
manager.UninstallPackage("com.android.app");
...
}
Запуск приложений
static void Main(string[] args)
{
...
client.StartApp(device, "com.android.app");
client.StopApp(device, "com.android.app"); // force-stop
...
}
Важные ссылки
AdvancedSharpAdbClient - основная библиотека
SharpAdbClient - была взята за основу, является форком madb
madb - портированная версия ddmlib с Java
ddmlib - оригинальная библиотека для работы с adb
P.S
Я не являюсь противником посредников, типа Appium, просто эти фреймворки больше больше подходят для тестирования приложений, а не автоматизации.
Я являюсь только начинающим разработчиком, поэтому прошу строго не судить правильность. кода
Если у вас появились какие либо вопросы, пишите мне на почту.
Это моя первая статья на Habr, поэтому заранее извиняюсь за косяки.
Комментарии (8)
amedvedjev
03.10.2021 15:04Для фанатов ADB есть https://github.com/ashishb/adb-enhanced.
Про Аппиум: 12 Android + 12 iPhpone тел при одновременном использовании кушают процессор современного масМини на 10-15%.
Ну а для себя вы конечно молодец.
yungd1plomat Автор
03.10.2021 19:50Речь идет не про тестирование, а конкретно про автоматизацию.При автоматизации используются эмуляторы, поэтому про проц на 10-12% можете забыть, как никак сам драйвер Appium жрет достаточно много, так ещё установка не очень удобная, конечно в сложных проектах Appium будет куда лучше, но для более простых думаю можно ограничиться и простым adb.Насчет репа который вы скинули вообще не предусматривает поиск элементов, это просто оболочка
amedvedjev
03.10.2021 21:51при автоматизации используют то, что просит заказчик.
вы можете привести пример автоматизации? просто интересно как сильно это отличается от тестирования.
ЗЫ 4 года назад у меня был пример автоматизации (как мин это я так понял). задача проверять логин реальным клиетном каждые 15мин. проблемы - отсылка по слаку в чем проблема. Умерла сама джоба в CI, неправильный экран после кого-то действия (какой вместо), не пришла за хх секунд смс-ка реально на тел. Продолжать по возможности логин - например смс-ку взять по апи или в базе.
yungd1plomat Автор
03.10.2021 21:53Автоматизация - например регистрация аккаунтов в приложении, это повторение одних и тех же действий бесконечно получая на выходе какой то результат
amedvedjev
03.10.2021 22:01так и в тестиравании мы каждый день делаем ночные раны. практичести бесконечно - пока проект не умрет -)
по мне ваш пример идеален для небольших задач. в Аппиме начало не такое быстрое, особенно с Аппл тел.
yungd1plomat Автор
03.10.2021 22:05Ну вообще вы правы, как я уже говорил библиотека создана для небольших проектов, тут внимание заострено на самом процессе, а не обработке результатов действий, поэтому и работает он чуток быстрее чем аппиум
Slav2
Нашел в описании библиотеки что есть еще функции создания скриншетов, но почему то в статье о ней ни слова. Она нестабильно работает?
yungd1plomat Автор
Работает стабильно и исправно