Всем привет

Не давно понадобилось автоматизировать одно приложение.Мне не хотелось использовать какие то посредники по типу 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)


  1. Slav2
    03.10.2021 14:23

    Нашел в описании библиотеки что есть еще функции создания скриншетов, но почему то в статье о ней ни слова. Она нестабильно работает?


    1. yungd1plomat Автор
      03.10.2021 14:57

      Работает стабильно и исправно


  1. amedvedjev
    03.10.2021 15:04

    Для фанатов ADB есть https://github.com/ashishb/adb-enhanced.

    Про Аппиум: 12 Android + 12 iPhpone тел при одновременном использовании кушают процессор современного масМини на 10-15%.

    Ну а для себя вы конечно молодец.


    1. yungd1plomat Автор
      03.10.2021 19:50

      Речь идет не про тестирование, а конкретно про автоматизацию.При автоматизации используются эмуляторы, поэтому про проц на 10-12% можете забыть, как никак сам драйвер Appium жрет достаточно много, так ещё установка не очень удобная, конечно в сложных проектах Appium будет куда лучше, но для более простых думаю можно ограничиться и простым adb.Насчет репа который вы скинули вообще не предусматривает поиск элементов, это просто оболочка


      1. amedvedjev
        03.10.2021 21:51

        при автоматизации используют то, что просит заказчик.

        вы можете привести пример автоматизации? просто интересно как сильно это отличается от тестирования.

        ЗЫ 4 года назад у меня был пример автоматизации (как мин это я так понял). задача проверять логин реальным клиетном каждые 15мин. проблемы - отсылка по слаку в чем проблема. Умерла сама джоба в CI, неправильный экран после кого-то действия (какой вместо), не пришла за хх секунд смс-ка реально на тел. Продолжать по возможности логин - например смс-ку взять по апи или в базе.


        1. yungd1plomat Автор
          03.10.2021 21:53

          Автоматизация - например регистрация аккаунтов в приложении, это повторение одних и тех же действий бесконечно получая на выходе какой то результат


          1. amedvedjev
            03.10.2021 22:01

            так и в тестиравании мы каждый день делаем ночные раны. практичести бесконечно - пока проект не умрет -)

            по мне ваш пример идеален для небольших задач. в Аппиме начало не такое быстрое, особенно с Аппл тел.


            1. yungd1plomat Автор
              03.10.2021 22:05

              Ну вообще вы правы, как я уже говорил библиотека создана для небольших проектов, тут внимание заострено на самом процессе, а не обработке результатов действий, поэтому и работает он чуток быстрее чем аппиум