Разделы портала

Онлайн-тренинги

.
Создание тестов для REST API на Python с использованием запросов. Часть 3: работа с XML
24.07.2020 00:00

Автор: Баз Дейкстра (Bas Djikstra)
Оригинал статьи
Перевод: Ольга Алифанова

Недавно я провел свой первый трехдневный курс "Python для тестировщиков". В нем я, в частности, раскрывал тему создания тестов REST API с использованием Python-библиотеки requests и фреймворка pytest для юнит-тестирования.

В этой короткой серии статей я хочу показать, как можно использовать Python-библиотеку запросов для создания тестов REST API. Это третья часть серии, и в ней мы рассмотрим работу с XML-телами запросов и ответов. В предыдущих частях рассказывалось, как приступить к работе с requests и pytest, а также о создании тестов, управляемых через данные.

REST API и XML

Большинство REST API, с которыми я нынче сталкиваюсь, работают с JSON в качестве предпочтительного формата данных для тел запросов и ответов. Однако время от времени вы будете встречаться с API, работающими с XML. Так как с XML, по сравнению с JSON, немного труднее работать в коде (не только в Python, а в целом), я решил, что будет неплохой идеей показать примеры создания тел запросов XML и разобраться, как парсить и создавать тесты для XML-тел ответов, работая с библиотекой requests.

Для примеров в статье я использовал операцию из REST API ParaBank, которая передает оплату счетов. Она доступна по адресу и включает, помимо двух параметров запроса, уточняющих источник (accountId) и сумму счета (amount), XML-тело запроса, содержащее информацию о человеке, которому отправляется платеж – то есть payee. Неудивительно, что тело запроса передается в API через HTTP POST.

Создание XML-тела запроса с использованием строк

Я хочу показать вам два разных подхода к созданию XML-тел запросов. Первый наиболее прямолинеен, но также и наименее гибок: это создание метода, возвращающего строку, содержащую тело XML.

  1. def fixed_xml_body_as_string():
  2. return """
  3. <payee>
  4. <name>John Smith</name>
  5. <address>
  6. <street>My street</street>
  7. <city>My city</city>
  8. <state>My state</state>
  9. <zipCode>90210</zipCode>
  10. </address>
  11. <phoneNumber>0123456789</phoneNumber>
  12. <accountNumber>12345</accountNumber>
  13. </payee>
  14. """

Заметьте, что двойные кавычки используются трижды, чтобы позволить вам объявить многострочную строку. Конечно, мы можем также читать тело из XML или текстового файла, который хранится в системе, вместо того, чтобы жестко кодировать его. Результат будет тем же.

Если нужно передать это XML-тело запроса в API, это можно сделать так:

  1. def test_send_xml_body_from_string_check_status_code_and_content_type():
  2. response = requests.post(
  3. "http://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500",
  4. headers={"Content-Type": "application/xml"},
  5. data=fixed_xml_body_as_string()
  6. )
  7. assert response.status_code == 200
  8. assert response.headers["Content-Type"] == "application/xml"

Отметьте, что мы явно указываем заголовок запроса Content-Type как application/xml, чтобы убедиться, что получатель понимает, что тело запроса должно интерпретироваться как XML. Отправка тела запроса XML происходит следующим образом: значение XML-строки, которую возвращает метод, приписывается параметру data метода requests post().

Чтобы проверить, что запрос успешно получен и обработан, мы убеждаемся, что код ответа 200, а заголовок ответа Content-Type имеет значение application/xml. С XML-телом ответа мы разберемся чуть позже.

Создание XML-тела запроса с использованием ElementTree

Другой подход к работе с XML-телом запроса – это их программное построение. В Python для этого есть мощная библиотека, ElementTree. Мы можем импортировать ее в наш модуль:

  1. import xml.etree.ElementTree as et

Так как XML-документ – это по сути дерево с корневым узлом, к которому крепятся дочерние узлы, мы начинаем создавать тело XML-запроса, определяя корневой узел payee:

  1. payee = et.Element('payee')

Затем мы определяем элемент name – дочерний элемент payee:

  1. name = et.SubElement(payee, 'name')

Нам также нужно назначить значение элемента name:

  1. name.text = 'John Smith'

Для примера это не требуется, но если вам нужно добавить атрибут (например, type) со значением fullName к элементу name, вы можете сделать это так:

  1. name.set('type', 'fullName')

Создание полного XML-тела запроса для нашего API-вызова заключается в повторе этих строк в правильном порядке с правильными значениями:

  1. def create_xml_body_using_elementtree():
  2. payee = et.Element('payee')
  3. name = et.SubElement(payee, 'name')
  4. name.text = 'John Smith'
  5. address = et.SubElement(payee, 'address')
  6. street = et.SubElement(address, 'street')
  7. street.text = 'My street'
  8. city = et.SubElement(address, 'city')
  9. city.text = 'My city'
  10. state = et.SubElement(address, 'state')
  11. state.text = 'My state'
  12. zip_code = et.SubElement(address, 'zipCode')
  13. zip_code.text = '90210'
  14. phone_number = et.SubElement(payee, 'phoneNumber')
  15. phone_number.text = '0123456789'
  16. account_number = et.SubElement(payee, 'accountNumber')
  17. account_number.text = '12345'
  18. return et.tostring(payee)

Заметьте, что нам нужно сконвертировать дерево элементов в строку, прежде чем использовать его с библиотекой requests. Это можно сделать при помощи метода tostring().

Подход ElementTree может выглядеть чуть более замороченным, чем простое определение XML как строки, однако он дает возможность создавать более сложные и гибкие XML-документы при помощи циклов, повторяющих блоки XML, работы с источниками данных, которые трансформируются в XML, и так далее. У меня нет четкого предпочтения в подходах, однако я считаю, что нужно знать об обоих вариантах и выбирать тот, который лучше подходит для ваших требований.

Чтобы использовать XML, созданный при помощи ElementTree, в качестве тела запроса, мы можем воспользоваться тем же способом, что и при создании строки, содержащей XML:

  1. def test_send_xml_body_from_elementtree_check_status_code_and_content_type():
  2. response = requests.post(
  3. "http://parabank.parasoft.com/parabank/services/bank/billpay?accountId=12345&amount=500",
  4. headers={"Content-Type": "application/xml"},
  5. data=create_xml_body_using_elementtree()
  6. )
  7. assert response.status_code == 200
  8. assert response.headers["Content-Type"] == "application/xml"

Интерпретация и работа с XML-телом ответа

Теперь, когда мы разобрались с созданием XML-тел запроса, давайте посмотрим, что можно сделать с XML-ответами. На данный момент самым мощным способом создания конкретных тестов будет конвертация XML-тела ответа в ElementTree, а затем – проверка его свойств.

В качестве примера выполним HTTP GET-вызов.

Ответ вернет детали учетной записи с ID 12345. Если мы хотим проверить, скажем, то, что корневой узел XML-ответа имеет имя account, и что у него отсутствуют свойства и текстовое значение, это можно сделать так:

  1. def test_check_root_of_xml_response():
  2. response = requests.get("http://parabank.parasoft.com/parabank/services/bank/accounts/12345")
  3. response_body_as_xml = et.fromstring(response.content)
  4. xml_tree = et.ElementTree(response_body_as_xml)
  5. root = xml_tree.getroot()
  6. assert root.tag == "account"
  7. assert len(root.attrib) == 0
  8. assert root.text is None

Заметьте, что сначала нам нужно сконвертировать XML-тело ответа в объект с типом Element, используя метод fromstring(), а затем – создать из этого ElementTree, используя конструктор ElementTree(), который принимает Element как аргумент.

Если нас интересует текстовое значение конкретного дочернего элемента XML-ответа – к примеру, customerId, содержащего ID пользователя, которому принадлежит аккаунт, это можно сделать через поиск элемента в ElementTree при помощи метода find() и последующего создания проверки свойства text у этого элемента:

  1. def test_check_specific_element_of_xml_response():
  2. response = requests.get("http://parabank.parasoft.com/parabank/services/bank/accounts/12345")
  3. response_body_as_xml = et.fromstring(response.content)
  4. xml_tree = et.ElementTree(response_body_as_xml)
  5. first_name = xml_tree.find("customerId")
  6. assert first_name.text == "12212"

Метод find() возвращает первое упоминание об этом элементе. Если нам нужно получить все элементы, совпадающие с этим именем, нужно использовать findall():

  1. def test_use_xpath_for_more_sophisticated_checks():
  2. response = requests.get("http://parabank.parasoft.com/parabank/services/bank/customers/12212/accounts")
  3. response_body_as_xml = et.fromstring(response.content)
  4. xml_tree = et.ElementTree(response_body_as_xml)
  5. savings_accounts = xml_tree.findall(".//account/type[.='SAVINGS']")
  6. assert len(savings_accounts) > 1

Как можно видеть, вместо передачи имен элементов напрямую можно воспользоваться выражениями XPath для более сложного выбора. Выражение

.//account/type[.='SAVINGS']

в примере выше выбирает все упоминания элемента type (дочернего элемента account), значение которых соответствует SAVINGS.

Использование примеров

Примеры кода из этой статьи можно найти на моей странице GitHub. Если вы скачаете проект и (при условии, что вы верно установили Python) запустите:

in the example above selects all occurrences of the type element (a child element of account) that have SAVINGS as their element value.

pip install -r requirements.txt

из корня проекта python-requests для установки нужных библиотек, вы сможете прогнать тесты самостоятельно.

Обсудить в форуме