Привет, Хабр! Меня зовут Михаил, я SDET-специалист компании SimbirSoft. Я занимаюсь автоматизацией тестирования, в основном это работа с WEB и REST API. Но на последнем проекте использовался SOAP, поэтому при автоматизации тестирования я работал с сообщениями этого протокола, а именно:
выполнять проверку наличия обязательных атрибутов и тегов SOAP сообщений;
сравнивать содержание различных сообщений;
вносить изменения или генерировать новые сообщения для исходящих запросов.
В своей статье я поделюсь несколькими способами работы с XML-документами. Материал будет полезен тем, кто впервые сталкивается в работе из кода с подобными документами на Java.
![](https://habrastorage.org/getpro/habr/upload_files/f5a/658/da2/f5a658da2b46d77aa9993a3fe913b08f.png)
Для работы я преобразовывал SOAP сообщения в Java-объекты. Этот процесс называется демаршаллинг. Обратный процесс называется маршаллинг, как десериализация и сериализация JSON.
Отличие маршалинга от сериализации заключается в том, что сериализация предполагает упаковку лишь данных программы. Например, при сериализации объекта класса, состоящего из полей и различных методов, сохраняться будет информация только о полях, а о методах — нет.
При маршалинге объекта запишется не только информацию о его данных (полях), но и информация по восстановлению структуры объекта — класс объекта, либо его мета-информацию для реконструирования типа.
В Java существует множество инструментов и способов преобразования документов формата XML в объекты. Я выбирал между использованием JAXB и DOM.
В качестве примера приведу XML-документ со следующим содержанием:
<?xml version="1.0" encoding="UTF-8"?>
<Screen Stage="Main">
<Title>Личный кабинет</Title>
<Description>Выберите действие</Description>
<Buttons>
<Button Action="CREATE_NEW_OFFER" Color="blue" Condition="true">
<T>Новый заказ</T>
<D Color="red">Заказать книгу</D>
<D>Заказать тетрадь</D>
</Button>
<Button Action="RETURN_PRODUCT" Color="red" Condition="true">
<T>Возврат</T>
<D>Оформить возврат</D>
</Button>
<Button Action="HISTORY" Color="grey" Condition="false">
<T>История покупок</T>
<D>За весь период</D>
<D>За последний месяц</D>
<D Color="Green"> За последний год</D>
</Button>
</Buttons>
<Attributes>
<Attribute A="BC">
<V>Атрибут №1</V>
</Attribute>
<Attribute A="DE">
<V>Атрибут №2</V>
</Attribute>
<Attribute A="FG">
<V>Атрибут №3</V>
</Attribute>
</Attributes>
</Screen>
Это описание экрана некоторого продукта.
Корневым элементом является <Screen>, содержащий в себе теги заглавия, описания экрана, а также блок кнопок и несколько атрибутов.
Кнопки, в свою очередь, содержат заглавия и различное количество подсказок. В некоторых тегах есть атрибуты, описывающие состояние и цвет элемента. А также команду “Action”, которую необходимо использовать при выполнении запроса на определенное действие.
У данного продукта все формируемые экраны соответствуют единой схеме, отличия могут быть в содержании тегов, их количестве и значениях атрибутов. К примеру, на экране может быть больше или меньше кнопок с различными состояниями, атрибутами и др.
JAXB
JAXB (Java Architecture for XML Binding) — это специальный инструмент для маршалинга и демаршалинга объектов.
JAXB предоставляет аннотации, которыми размечаются поля JAVA-классов.
Для начала необходимо создать требуемые классы.
Принцип получается примерно следующий: если тег имеет дочерние элементы или атрибуты, значит это объект. Если тег в документе не имеет дочерних элементов, значит это поле объекта.
Корневой класс Screen:
public class Screen {
private String stage;
private String title;
private String description;
private List<Button> buttonList = new ArrayList<>();
private List<Attribute> attributeList = new ArrayList<>();
}
Он содержит в себе три строковых поля: “stage”, “title”, “description” и коллекции “buttonList” и “attributeList”.
3 класса вложенных элементов:
public class Button {
private String action;
private String color;
private String condition;
private String t;
private List<D> dList = new ArrayList<>();
}
public class Attribute {
private String a;
private String v;
}
public class D {
private String color;
private String d;
}
Всего нужно 4 класса.
Теперь нам необходимо разметить наши поля и классы аннотациями, чтобы иметь возможность выполнять операции маршаллинга и демаршаллинга.
В этом случае нам понадобятся следующие аннотации:
— @XmlRootElement(name = "Screen") — определяет корневой элемент. Может быть указана над классом или полем класса;
— @XmlAccessorType(XmlAccessType.FIELD) — определяет сущности, которые используются в процессах преобразования;
— @XmlAttribute(name = "Stage") — определяет, что поле является атрибутом, а не тегом в XML-документе;
— @XmlElement(name = "Title") — определяет, что поле является тегом в XML-документе;
— @XmlElementWrapper(name="Buttons") — создает некоторую обертку вокруг группы повторяющихся тегов;
— @XMLValue — определяет, что поле содержит в себе значение тега.
Получились следующие классы:
@Data
@XmlRootElement(name = "Screen")
@XmlAccessorType(XmlAccessType.FIELD)
public class Screen {
@XmlAttribute(name = "Stage")
private String stage;
@XmlElement(name = "Title")
private String title;
@XmlElement(name = "Description")
private String description;
@XmlElementWrapper(name="Buttons")
@XmlElement(name="Button")
private List<Button> buttonList;
@XmlElementWrapper(name="Attributes")
@XmlElement(name="Attribute")
private List<Attribute> attributeList;
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class Attribute {
@XmlAttribute(name = "A")
private String a;
@XmlElement(name = "V")
private String v;
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class Button {
@XmlAttribute(name = "Action")
private String action;
@XmlAttribute(name = "Color")
private String color;
@XmlAttribute(name = "Condition")
private String condition;
@XmlElement(name = "T")
private String t;
@XmlElement(name="D")
private List<D> dList = new ArrayList<>();
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class D {
@XmlAttribute(name = "Color")
private String color;
@XmlValue
private String d;
}
В аннотациях, в параметре “name”, указывается имя тега XML, значение которого мы присваиваем полю объекта при маршаллинге.
А при демаршалинге этот параметр, наоборот, задает имя тега XML для значения поля объекта.
В примере сеттеры и геттеры реализованы через аннотацию Lombok @Data. Но в JAXB они не требуются и не используются. Таким образом, отсутствует возможность редактирования значений полей при преобразованиях. Но они нужны для других стандартных операций с объектами.
Для удобства во всех классах переопределен метод toString(). Вот так этот метод реализован в классе Screen:
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("<Screen Stage='");
sb.append((this.stage == null) ? "null" : this.stage);
sb.append("'> \n");
sb.append(" <Title>");
sb.append((this.title == null) ? "null" : this.title);
sb.append("</Title> \n");
sb.append(" <Description>");
sb.append((this.description == null) ? "null" : this.description);
sb.append("</Description> \n");
sb.append(" <Buttons> \n");
for(Button button : buttonList) {
sb.append(button);
}
sb.append(" </Buttons> \n");
sb.append(" <Attributes> \n");
for(Attribute attribute : attributeList) {
sb.append(attribute);
}
sb.append(" </Attributes> \n");
sb.append("</Screen");
return sb.toString();
}
В остальных классах он переопределен аналогичным образом.
Классы готовы к работе.
Демаршаллинг
Реализуем чтение документа XML. Для этого используем BufferedReader c чтением из файла и StringReader, который будет считывать созданную строку в процессе преобразования.
После создаем JAXBContext — сущность, которая лежит в основе работы процессов маршаллинга и демаршаллинга. Это точка входа в API JAXB, она предоставляет абстракцию для управления информацией о привязке XML/Java. В качестве параметра при создании указываем класс получаемого объекта — Screen.class.
С помощью JAXBContext создадим Unmarshaller, необходимый для преобразования XML-документа в объект Screen с приведением типа. Для этого в метод unmarshal() передается ранее созданный StringReader:
public class Main {
public static void main(String[] args) {
BufferedReader br = new BufferedReader(new FileReader("./body.xml"));
String body = br.lines().collect(Collectors.joining());
StringReader reader = new StringReader(body);
JAXBContext context = JAXBContext.newInstance(Screen.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
Screen screen = (Screen) unmarshaller.unmarshal(reader);
}
}
Результат можно увидеть, развернув объект в окне отладки, или путем выведения в консоль — для чего и переопределялись методы toString().
Это был процесс демаршаллинга. Из документа (сообщения SOAP) получили объект Java с методами для работы, всеми полями и атрибутами.
Маршалинг
Создаем StringWriter, который будет записывать строку.
Также создадим сущность Marshaller с установкой параметров форматирования данных.
Чтобы проверить, что все работает, внесем изменения в полученный ранее объект screen: изменяется заголовок и цвет с описанием для первой кнопки.
После чего в метод marshal() передаем объект и StringWriter:
StringWriter writer = new StringWriter();
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
screen.setTitle("Изменили заглавие");
screen.getButtonList().get(0).getDList().get(0).setColor("black");
screen.getButtonList().get(0).getDList().get(0).setD("Цвет кнопки черный");
marshaller.marshal(screen, writer);
Теперь writer хранит в себе данные объекта screen в виде строки, с помощью которой можно перезаписать или создать новый XML-документ, либо сгенерировать сообщение SOAP.
Для просмотра результата можно воспользоваться окном отладки или вывести в консоль.
Более подробно с JAXB можно ознакомиться тут.
DOM
Второй способ преобразований с использованием DOM (Document Object Model) в сравнении с предыдущим может показаться более трудным и запутанным. Здесь не используются аннотации для поиска и определения тегов и полей классов. При использовании DOM весь XML-документ представлен в виде древовидной структуры. Для перемещения по этой модели, получения узлов, значений имен тегов, их атрибутов и т.д. существуют специальные методы и сущности.
Сначала также создаем классы:
@Data
public class ScreenDom {
private HashMap<String, String> attributes = new HashMap<>();
private String title;
private String description;
private List<ButtonDom> buttonListt = new ArrayList<>();
private List<AttributeDom> attributeList = new ArrayList<>();
}
@Data
public class ButtonDom {
private HashMap<String, String> attributes = new HashMap<>();
private String t;
private List<DDom> dList = new ArrayList<>();
}
@Data
public class AttributeDom {
private HashMap<String, String> attributes = new HashMap<>();
private String v;
}
@Data
public class DDom {
private HashMap<String, String> attributes = new HashMap<>();
private String d;
public DDom withD(String value) {
this.d = value;
return this;
}
}
Основное отличие заключается в том, что для хранения атрибутов тега используется HashMap<> attributes. Теперь не важно, какие именно могут быть атрибуты у тега — при преобразовании они все будут добавлены в мапу в формате «ключ-значение».
Здесь гетеры и сеттеры используются при преобразованиях. Можно написать свои и при необходимости реализовать в них изменения входных и выходных параметров.
Для начала немного об определениях некоторых сущностей, которые нам понадобятся.
Важно понять, что основной «атомарной» единицей в этой модели является Node — узел.
Узлом может быть:
Node — класс, который предназначен для любого XML-элемента: текст, тег, или атрибут. Т.е. все в XML есть Node. Далее идет специализация.
NamedNodeMap — сущность, в которой хранятся данные «ключ-значение», она используется при получении атрибутов тега.
NodeList — список узлов, используемый при получении дочерних элементов и узлов с одинаковыми тегами.
Пример кода по заполнению объекта данными из XML-документа с использованием DOM:
public class Main {
public static void main(String[] args) {
//Создаем объект.
ScreenDom screen = new ScreenDom();
//Для создания древовидной структуры создается объект класса DocumentBuilder
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
//Создается объект Document — он является представлением всей информации внутри XML
Document document = builder.parse("./body.xml");
//Создается узел root путем получения первого дочернего узла. Это будет тег <Screen>
Node root = document.getFirstChild();
//Создается нода, в которую сохраняются все атрибуты корневого тега
NamedNodeMap attributes = root.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
screen.getAttributes().put(attributes.item(i).getNodeName(), attributes.item(i).getNodeValue());
}
//Создается список всех дочерних узлов
NodeList childNode = root.getChildNodes();
//В цикле выполняется поиск и присвоение значений согласно заданным условиям
for (int i = 0; i < childNode.getLength(); i++) {
Node element = childNode.item(i);
if (element.getNodeName().equals("Title")) {
screen.setTitle(element.getTextContent());
}
if (element.getNodeName().equals("Description")) {
screen.setDescription(element.getTextContent());
}
}
// Создается список узлов по тегу <Button>. Их может быть несколько
NodeList rootList = document.getDocumentElement().getElementsByTagName("Button");
//Внешний цикл списка всех узлов <Button>
for (int j = 0; j < rootList.getLength(); j++) {
//Извлекаются все атрибуты для тега <Button> текущей итерации
attributes = rootList.item(j).getAttributes();
//Создается новый объект ButtonDom для сохранения найденных значений
screen.getButtonList().add(new ButtonDom());
for (int i = 0; i < attributes.getLength(); i++) {
screen.getButtonList().get(j).getAttributes().put(attributes.item(i).getNodeName(), attributes.item(i).getNodeValue());
}
//Создается список всех дочерних узлов
childNode = rootList.item(j).getChildNodes();
//В цикле выполняется поиск и присвоение значений дочерних узлов
//к объекту ButtonDom текущей итерации
for (int i = 0; i < childNode.getLength(); i++) {
Node element = childNode.item(i);
NamedNodeMap attr = element.getAttributes();
if (element.getNodeName().equals("T")) {
screen.getButtonList().get(j).setT(element.getTextContent());
}
if (element.getNodeName().equals("D")) {
DDom d = new DDom();
d.setD(element.getTextContent());
// В случае наличия атрибутов у тегов <D> они добавляются в мапу
if (attr.getLength() != 0) {
for (int k = 0; k < attr.getLength(); k++) {
d.getAttributes().put(attr.item(k).getNodeName(), attr.item(k).getNodeValue());
}
}
// Объект DDom добавляется в ButtonsDom текущей итерации первого цикла
screen.getButtonList().get(j).getDList().add(d);
}
}
}
rootList = document.getDocumentElement().getElementsByTagName("Attribute");
for (int j = 0; j < rootList.getLength(); j++) {
AttributeDom attributeDom = new AttributeDom();
attributes = rootList.item(j).getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
attributeDom.getAttributes().put(attributes.item(i).getNodeName(), attributes.item(i).getNodeValue());
}
childNode = rootList.item(j).getChildNodes();
for (int i = 0; i < childNode.getLength(); i++) {
Node element = childNode.item(i);
if (element.getNodeName().equals("V")) {
attributeDom.setV(element.getTextContent());
}
}
screen.getAttributeList().add(attributeDom);
//Итоговым объектом является заполненная сущность screen класса Screen
}
}
Для удобства можно написать специальные классы-парсеры. В приведенном выше коде представлены некоторые методы, которые возможно для этого использовать.
Как такового обратного процесса, как в случае с JAXB, этот способ не подразумевает. Но есть переопределенный метод toString() объекта ScreenDom, который возвращает строку формата XML. Привести ее к нужному формату уже не составит труда.
К дополнению метода описанного выше также используется XPath.
XPath — это язык запросов к элементам XML. Он позволит создать выражение запроса и получить интересующие данные из документа.
В своей работе при тестировании API я использую RestAssured и его поисковые выражения.
Чтобы получить значение «Атрибут №1» из Response RestAssured, можно использовать следующий код:
String value = response.xmlPath().getString(“**.find {it.@A==’BC’}”);
Поисковое выражение прочитаем следующим образом: во всем документе найти значение первого тега, у которого есть атрибут “A”, равный значению “BC”.
Есть и другие способы и выражения XPath, поддерживаемые RestAssured:
root.parent.child.grandchild;.
root.parant.child[0];
(child[0]);
root.parent[0];
it.@type == 'специфическое значение';
Вышеописанный способ отлично подходит для поиска определенных значений в XML-документе. Он не требует создания сущностей Java, применяется довольно легко и быстро. Но стоит отметить, что данный способ не подходит для полного процесса преобразования из документа в объект и обратно.
Более подробно об использовании Xpath в RestAssured вы можете ознакомиться в документации.
Весь код статьи доступен в удаленном репозитории по этой ссылке.
Это рабочий проект, в котором реализованы оба способа маршаллинга и демаршаллинга. Кроме того, в нем реализован тест, пример возможного использования данных методов при автоматизации тестирования сообщений SOAP- или XML-документов.
Заключение
В этой статье я поделился своим опытом в решении некоторых задач, с которыми столкнулся при автоматизации тестирования сообщений SOAP. Это далеко не все способы и инструменты, доступные для преобразования XML на Java. Но они работают и их довольно легко понять и реализовать.
Каждый способ имеет свои плюсы и минусы. Для удобного восприятия я представил их в таблице ниже:
Инструмент | Плюсы | Минусы |
JAXB | Прост в реализации. Легкочитаемый код. Отлично подходит, если используется XML-схема. Возможно частичное использование. | Нужно точно знать возможные атрибуты тегов. Нет возможности редактировать значения при преобразовании объектов.
|
DOM | Нет необходимости заранее знать возможные атрибуты тегов. Возможность редактирования значений при преобразовании объектов. Гибкость в использовании. | Сложная реализация. Иногда трудночитаемый код. |
В своем случае я выбрал DOM из-за неизвестности атрибутов тегов в получаемых сообщениях SOAP. Мне понадобилась возможность добавлять новые узлы в древовидную структуру документа при преобразованиях. При помощи методов DOM я реализовал классы — парсеры, которые позволили достаточно легко и просто выполнить маршаллинг и демаршлаллинг по необходимым мне условиям.
Надеюсь, эта статья была вам интересна. Спасибо за внимание!
Рекомендуем другие наши SDET-статьи:
Ускоряем работу с тестовой документацией. Экспорт данных из Allure-отчета в Confluence
Как настроить Pipeline для Jenkins, Selenoid, Allure
Что дает автоматизация тестирования
От пирамиды тестов – к колесу автоматизации: какие проверки нужны на проекте
Авторские материалы для разработчиков мы также публикуем в наших соцсетях – ВКонтакте и Telegram.