Привет, Хабр!
Работаю на большом интеграционном проекте (IBM MQ, WebSphere, Oracle) и оплетаю наш кровавый энтерпрайз паутиной функциональных тестов в JMeter, который крутится на тестовом стенде и пробуждается по зову Jenkins после деплоя нового билда. По мере увеличения количества тестов столкнулся с проблемой поддержания тестовой документации в актуальном виде.
Само дерево тестов в JMeter по сути является документом — трэды разбивают функциональность на логические куски, внутри трэдов контроллеры содержат тесты, а каждый сэмплер внутри контроллера — отдельный шаг. Иерархия объектов четко пронумерована, за исключением служебных штук навроде ассершенов, таймеров и прочего менее интересного с точки зрения бизнес-логики.
В итоге получается достаточно аккуратная картинка:
Впрочем, не каждый менеджер готов запустить у себя JMeter для просмотра положения дел в области QA. Исторически сложилось, что вся документация проекта ведется в Confluence.
Я не был готов вручную копипастить описание тест-кейсов на страницу Confluence после разработки их в JMeter. Отчаянные гугления не дали результата — не обнаружил готового и легкого решения для экспорта дерева объектов из JMeter в текст (если таки есть, напиши о нем в комментарии, пожалуйста, а я посыплю голову пеплом из ачивки "умею в гугл").
Заглянув во внутренности JMX-файла (стандартное расширение тест-плана JMeter), обнаружил, что все интересные мне объекты отмечены атрибутом testname:
<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>
Осталось дело за малым — написать парсер, который:
- Добудет желанный текст с описанием шага\теста\группы из JMX-файла
- Выкинет строки с описанием неинтересных объектов (ассершены, таймеры и прочее)
- Запишет всё по порядку в файл, чтобы актуализация документа включала один сиротливый копипаст
С пунктом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.
Берегите своё время. И спасибо за внимание.
Комментарии (9)
sand14
07.06.2018 16:12+1Еще порадовало, что вы корректно освобождаете неуправляемые ресурсы, и для BufferedReader сделали это с помощью The try-with-resources Statement
Желательно так же сделать и для FileWriter, вместо примененного классического бойлеплейта с try-finally.
И еще замечание:
Объекты типа File не следовало помещать в контанты. От константы здесь только неизменяемость ссылки на объект.
А сам объект — "сложный", с изменяемым состоянием, и, хуже, того, позволяющий изменять внешний неуправляемый ресурс (физический файл).
В константы здесь нужно выносить пути к файлам, а сами файлы (File) создавать в коде.
sand14
07.06.2018 16:18И если уж совсем по-хорошему, стоит все вынести из метода main класса приложения в отдельный осмысленный по обязанности (см. SOLID) класс, объявить в нем один основной публичный метод, и декомпозировать его на пару приватных методов — работу с BufferedReader и FileWriter.
И в обработке исключений и прочих ситуаций не обращаться напрямую к выводу в консоль, а прокидывать в методы лямбду-стратегию (или создать в классе поле-стратегию) для делегирования логгирования.
sand14
Коллега, насколько понимаю, вы занимаетесь (авто)тестированием и увидели потенциал еще большей замены мануального тестирования на программное, что отрадно (и то то, делитесь опытом, тоже).
Комментирование работы с JMeter как таковым оставлю коллегам, компетентным в этой области, а со своей стороны поговорю о предложенном Java коде.
В целом хорошо написано для быстрого опыта, при этим есть, что улучшать:
Самая крупная ошибка (и, пожалуй, единственная) ошибка — то, что вы пытаетесь распарсивать XML файл вручную построчно.
Почему так не надо делать — долго объяснять, почитайте об XML и XSD.
А как надо: читать XML-файл с помощью XMLReader, или преобразовав его в DOM.
rahna Автор
Благодарю за замечание!
sand14
А поскольку JMX, судя по примеру файла в статье, реализует свой формат поверх XML (Сущность — Коллекция свойств — «Ключ — значение»), то, возможно, есть готовый движок чтения JMX, что будет проще, чем реализовывать то же самое с помощью XMLReader или DOM.
И кстати, не совсем ясно, что именно вас уберегло от использования XPath.
Если нет готового движка, то XPath — еще один способ читать этот файл, и он проще в написании/чтении/поддержке, чем XMLReader/DOM.
rahna Автор
Признаться честно, о поиске готового движка для чтения JMX-файлов не подумал. Для моей маленькой задачи этого не требовалось, однако это отличная идея для полезного досуга.
Регулярные выражения показались достаточно универсальным и надежным средством. Я с ними практически не имел прежде дел и это было еще одним поводом «покурить» тему.sand14
Изучить тему — да.
Но, если говорить о коммите своих опытов в прод, то, работая через регулярки с XML, вы спускаетесь на другой, более «физический» уровень абстракции работы с XML, реализуя все вещи самостоятельно.
Предусмотреть все в своем «велосипеде» не получится — например, предусматривают ли разработанные выражения парсинг для случая, когда содержимое элемента содержит экранированные символы (чтобы не возникло конфликта с тегами разметки)?
rahna Автор
sand14
В том то и дело, что фильтруя элементы по значению в XPath, XMLReader, или пробегаясь по DOM-дереву, вы можете искать именно символ ">".
А реализуя свой парсинг, придется учитывать правила экранирования спецсимволов, и искать что-то вроде %код_символа%.
Или как вы будете (и будете ли) обрабатывать тег !CDATA (в который можно поместить текст «как есть» без экранирования)? — выведете !CDATA[текст] вместо «текст»?