Привет, Хабр!


Работаю на большом интеграционном проекте (IBM MQ, WebSphere, Oracle) и оплетаю наш кровавый энтерпрайз паутиной функциональных тестов в JMeter, который крутится на тестовом стенде и пробуждается по зову Jenkins после деплоя нового билда. По мере увеличения количества тестов столкнулся с проблемой поддержания тестовой документации в актуальном виде.


Само дерево тестов в JMeter по сути является документом — трэды разбивают функциональность на логические куски, внутри трэдов контроллеры содержат тесты, а каждый сэмплер внутри контроллера — отдельный шаг. Иерархия объектов четко пронумерована, за исключением служебных штук навроде ассершенов, таймеров и прочего менее интересного с точки зрения бизнес-логики.


В итоге получается достаточно аккуратная картинка:


image

Впрочем, не каждый менеджер готов запустить у себя JMeter для просмотра положения дел в области QA. Исторически сложилось, что вся документация проекта ведется в Confluence.
Я не был готов вручную копипастить описание тест-кейсов на страницу Confluence после разработки их в JMeter. Отчаянные гугления не дали результата — не обнаружил готового и легкого решения для экспорта дерева объектов из JMeter в текст (если таки есть, напиши о нем в комментарии, пожалуйста, а я посыплю голову пеплом из ачивки "умею в гугл").


Заглянув во внутренности JMX-файла (стандартное расширение тест-плана JMeter), обнаружил, что все интересные мне объекты отмечены атрибутом testname:


Образец кусочка JMX-файла
        <AuthManager guiclass="AuthPanel" testclass="AuthManager" testname="1.4.2 Авторизоваться на портале" enabled="true">
          <collectionProp name="AuthManager.auth_list">
            <elementProp name="" elementType="Authorization">
              <stringProp name="Authorization.url">http://${ipKvp}:${portKvp}/TKVPImportTemporary</stringProp>
              <stringProp name="Authorization.username">${userKvp}</stringProp>
              <stringProp name="Authorization.password">${passKvp}</stringProp>
              <stringProp name="Authorization.domain">${domainKvp}</stringProp>
              <stringProp name="Authorization.realm"></stringProp>
            </elementProp>
          </collectionProp>
          <boolProp name="AuthManager.clearEachIteration">true</boolProp>
        </AuthManager>

Осталось дело за малым — написать парсер, который:


  1. Добудет желанный текст с описанием шага\теста\группы из JMX-файла
  2. Выкинет строки с описанием неинтересных объектов (ассершены, таймеры и прочее)
  3. Запишет всё по порядку в файл, чтобы актуализация документа включала один сиротливый копипаст

С пунктом1 успешно справилось регулярное выражение:


(?<=testname=\")(.*)(?=\" )

От использования xpath-селектора меня уберег рефлекс не использовать xpath, приобретенный в процессе написания селекторов для Selenium-тестов.


Так как я не нумеровал служебные объекты в дереве, пункт2 удалось реализовать без проблем в цикле, в котором:


  • достаю первый символ строки
  • привожу к int
  • в случае успеха записываю строку в список
  • иначе игнорирую
        try (BufferedReader br = new BufferedReader(new FileReader(JMX_FILE))) {
            String line;
            while ((line = br.readLine()) != null) {
                Matcher m1 = p.matcher(line);
                if (m1.find()) {
                    try {
                        Integer.parseInt(m1.group().substring(0, 1));
                        matchd.add(m1.group());
                    } catch (NumberFormatException e) {
                        System.out.println(m1.group().substring(0, 1) + ": excluding non-number string");
                    }
                }
            }
        }

И поскольку обработка файла идет подряд сверху вниз + нумерация объектов в дереве подчиняется четкой логике, ничего страшного для пункта3 также придумывать не пришлось:


        FileWriter writer = null;
        try {
            writer = new FileWriter(RESULT_FILE);
            for (String str : matchd) {
                writer.write(str + "\n");
            }
        } finally {
            if (writer != null) {
                writer.close();
            }
        }

Итоговый результат уместился в один маленький (~50 строк) класс:


Исходный код
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class App {

    private static final String SAMPLER_NAME_REGEXP = "(?<=testname=\")(.*)(?=\" )";
    private static final File JMX_FILE = new File("C:\\temp\\Test-plan.jmx");
    private static final File RESULT_FILE = new File("C:\\temp\\output.txt");

    public static void main(String[] args) throws IOException {
        Pattern p = Pattern.compile(SAMPLER_NAME_REGEXP);
        List<String> matchd = new ArrayList<>();

        try (BufferedReader br = new BufferedReader(new FileReader(JMX_FILE))) {
            String line;
            while ((line = br.readLine()) != null) {
                Matcher m1 = p.matcher(line);
                if (m1.find()) {
                    try {
                        Integer.parseInt(m1.group().substring(0, 1));
                        matchd.add(m1.group());
                    } catch (NumberFormatException e) {
                        System.out.println(m1.group().substring(0, 1) + ": excluding non-number string");
                    }
                }
            }
        }

        if (RESULT_FILE.delete()) {
            System.out.println("Deleting previous result file");
        } else {
            System.out.println("Creating new result file");
        }

        FileWriter writer = null;
        try {
            writer = new FileWriter(RESULT_FILE);
            for (String str : matchd) {
                writer.write(str + "\n");
            }
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }
}

В порядке эксперимента пробовал интегрировать этот код непосредственно в тест-план JMeter, но столкнулся с проблемами непонимания дженериков и импортов, и решил пока удовлетвориться вызовом полученного экспортера дерева в IDEA.


Берегите своё время. И спасибо за внимание.


image

Комментарии (9)


  1. sand14
    07.06.2018 13:07

    Коллега, насколько понимаю, вы занимаетесь (авто)тестированием и увидели потенциал еще большей замены мануального тестирования на программное, что отрадно (и то то, делитесь опытом, тоже).


    Комментирование работы с JMeter как таковым оставлю коллегам, компетентным в этой области, а со своей стороны поговорю о предложенном Java коде.


    В целом хорошо написано для быстрого опыта, при этим есть, что улучшать:


    Самая крупная ошибка (и, пожалуй, единственная) ошибка — то, что вы пытаетесь распарсивать XML файл вручную построчно.
    Почему так не надо делать — долго объяснять, почитайте об XML и XSD.


    А как надо: читать XML-файл с помощью XMLReader, или преобразовав его в DOM.


    1. rahna Автор
      07.06.2018 13:18
      +1

      Благодарю за замечание!


    1. sand14
      07.06.2018 13:26

      А поскольку JMX, судя по примеру файла в статье, реализует свой формат поверх XML (Сущность — Коллекция свойств — «Ключ — значение»), то, возможно, есть готовый движок чтения JMX, что будет проще, чем реализовывать то же самое с помощью XMLReader или DOM.

      И кстати, не совсем ясно, что именно вас уберегло от использования XPath.
      Если нет готового движка, то XPath — еще один способ читать этот файл, и он проще в написании/чтении/поддержке, чем XMLReader/DOM.


      1. rahna Автор
        07.06.2018 13:58

        Признаться честно, о поиске готового движка для чтения JMX-файлов не подумал. Для моей маленькой задачи этого не требовалось, однако это отличная идея для полезного досуга.

        И кстати, не совсем ясно, что именно вас уберегло от использования XPath.
        Регулярные выражения показались достаточно универсальным и надежным средством. Я с ними практически не имел прежде дел и это было еще одним поводом «покурить» тему.


        1. sand14
          07.06.2018 14:09

          Регулярные выражения показались достаточно универсальным и надежным средством. Я с ними практически не имел прежде дел и это было еще одним поводом «покурить» тему.

          Изучить тему — да.
          Но, если говорить о коммите своих опытов в прод, то, работая через регулярки с XML, вы спускаетесь на другой, более «физический» уровень абстракции работы с XML, реализуя все вещи самостоятельно.
          Предусмотреть все в своем «велосипеде» не получится — например, предусматривают ли разработанные выражения парсинг для случая, когда содержимое элемента содержит экранированные символы (чтобы не возникло конфликта с тегами разметки)?


          1. rahna Автор
            07.06.2018 15:02

            предусматривают ли разработанные выражения парсинг для случая, когда содержимое элемента содержит экранированные символы (чтобы не возникло конфликта с тегами разметки)?
            Если я правильно понимаю, о чем вы — нет, не предусматривает. Если в названиях объектов использовать сложные символы (например, >), JMeter «под капотом» их сконвертирует и использование xpath эту проблему также не решит.


            1. sand14
              07.06.2018 16:02

              В том то и дело, что фильтруя элементы по значению в XPath, XMLReader, или пробегаясь по DOM-дереву, вы можете искать именно символ ">".
              А реализуя свой парсинг, придется учитывать правила экранирования спецсимволов, и искать что-то вроде %код_символа%.
              Или как вы будете (и будете ли) обрабатывать тег !CDATA (в который можно поместить текст «как есть» без экранирования)? — выведете !CDATA[текст] вместо «текст»?


  1. sand14
    07.06.2018 16:12
    +1

    Еще порадовало, что вы корректно освобождаете неуправляемые ресурсы, и для BufferedReader сделали это с помощью The try-with-resources Statement


    Желательно так же сделать и для FileWriter, вместо примененного классического бойлеплейта с try-finally.


    И еще замечание:
    Объекты типа File не следовало помещать в контанты. От константы здесь только неизменяемость ссылки на объект.
    А сам объект — "сложный", с изменяемым состоянием, и, хуже, того, позволяющий изменять внешний неуправляемый ресурс (физический файл).
    В константы здесь нужно выносить пути к файлам, а сами файлы (File) создавать в коде.


  1. sand14
    07.06.2018 16:18

    И если уж совсем по-хорошему, стоит все вынести из метода main класса приложения в отдельный осмысленный по обязанности (см. SOLID) класс, объявить в нем один основной публичный метод, и декомпозировать его на пару приватных методов — работу с BufferedReader и FileWriter.
    И в обработке исключений и прочих ситуаций не обращаться напрямую к выводу в консоль, а прокидывать в методы лямбду-стратегию (или создать в классе поле-стратегию) для делегирования логгирования.