Привет, Хабр!
Работаю на большом интеграционном проекте (IBM WAS, WebSphere MQ, 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.
Берегите своё время. И спасибо за внимание.

