2025, Oct 16 08:18
TypeError в xml.etree: как сериализовать JUnit XML правильно
Разбираем, почему xml.etree.ElementTree падает с TypeError при записи JUnit XML: атрибуты должны быть строками. Пример исправления в Python. С кодом примера.
При формировании XML в стиле JUnit на Python с помощью xml.etree легко споткнуться о мелочь, которая ломает сериализацию: значения атрибутов должны быть строками. Если какой‑то атрибут окажется int, запись завершится ошибкой TypeError. Ниже — разбор конкретного сценария сбоя и минимального исправления, которое возвращает генерацию XML в норму.
Постановка задачи
Цель — получить такую структуру XML:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="65" failures="0" disabled="0" errors="0" name="AllTests">
<testsuite name="Tests" tests="65" failures="0" disabled="0" skipped="0" errors="0">
<testcase name="TestInit" file="" status="run" result="completed" />
<testcase name="TestAlways" file="" status="run" result="completed" />
</testsuite>
...
</testsuites>
Ниже — фрагмент Python-кода, который собирает XML из объектов в памяти и записывает его на диск. Он демонстрирует паттерн, приводящий к сбою.
import xml.etree.cElementTree as ET
class CaseNode:
def __init__(self, label, outcome):
self.name = label
self.result = outcome
def __str__(self):
return f"Test Case: {self.name}, Status: {self.result}"
class SuiteNode:
def __init__(self, title):
self.name = title
self.test_case_list = []
def __str__(self):
return f"Test Suite: {self.name}"
def run():
print("Hello from html-test-report-generator!")
suites = build_html_report()
for s in suites:
print(s)
for c in s.test_case_list:
print(c)
print()
write_xml_report(suites)
def build_html_report():
# создает список объектов SuiteNode и возвращает его вызывающей стороне
pass
def write_xml_report(suites):
root_suites = ET.Element(
"testsuites",
tests=len(suites), # <-- проблемное место: целочисленный атрибут
failures="0",
disabled="0",
errors="0",
name="AllTests",
)
for s in suites:
suite_node = ET.SubElement(
root_suites,
"testsuite",
name=s.name,
tests=len(s.test_case_list), # <-- проблемное место: целочисленный атрибут
failures="0",
disabled="0",
skipped="0",
errors="0",
)
for c in s.test_case_list:
ET.SubElement(
suite_node,
"testcase",
name=c.name,
file="",
line="",
status="run",
result=c.result,
)
tree = ET.ElementTree(root_suites)
tree.write("filename.xml")
Запуск этого кода приводит к исключению во время сериализации:
TypeError: cannot serialize 23 (type int)
Иногда его сопровождает сообщение вроде:
AttributeError: 'str' object has no attribute 'write'
Второе возникает из‑за попытки записать некорректное дерево и всплывает через внутренние обработчики. Суть проблемы — именно TypeError: ElementTree отказывается сериализовать целочисленный атрибут.
Что на самом деле происходит
В xml.etree значения атрибутов — это строки. Когда вы передаёте Python-целое число в атрибут (например, tests=len(...)), сериализатор не выполняет неявное преобразование и выбрасывает TypeError: cannot serialize 23 (type int). Сбой случается на этапе записи, когда ElementTree разворачивает структуру в памяти в последовательность байтов. Поскольку дерево по его правилам некорректно, сериализация останавливается.
Это легко подтвердить минимальным примером, который воспроизводит ту же ошибку:
import xml.etree.cElementTree as ET
root = ET.Element("testsuites", tests=23) # целочисленный атрибут
ET.ElementTree(root).write("filename.xml") # приводит к TypeError
А вот успешный вариант после явного преобразования в строку:
import xml.etree.cElementTree as ET
root = ET.Element("testsuites", tests=str(23)) # строковый атрибут
ET.ElementTree(root).write("filename.xml") # работает
Исправляем генерацию XML
Лечение простое: передавать в Element или SubElement строковые значения атрибутов, заранее преобразовав числа в строки. В исходном коде нужно поправить атрибут tests в двух местах.
import xml.etree.cElementTree as ET
class CaseNode:
def __init__(self, label, outcome):
self.name = label
self.result = outcome
def __str__(self):
return f"Test Case: {self.name}, Status: {self.result}"
class SuiteNode:
def __init__(self, title):
self.name = title
self.test_case_list = []
def __str__(self):
return f"Test Suite: {self.name}"
def run():
print("Hello from html-test-report-generator!")
suites = build_html_report()
for s in suites:
print(s)
for c in s.test_case_list:
print(c)
print()
write_xml_report(suites)
def build_html_report():
# создает список объектов SuiteNode и возвращает его вызывающей стороне
pass
def write_xml_report(suites):
root_suites = ET.Element(
"testsuites",
tests=str(len(suites)), # преобразование в строку
failures="0",
disabled="0",
errors="0",
name="AllTests",
)
for s in suites:
suite_node = ET.SubElement(
root_suites,
"testsuite",
name=s.name,
tests=str(len(s.test_case_list)), # преобразование в строку
failures="0",
disabled="0",
skipped="0",
errors="0",
)
for c in s.test_case_list:
ET.SubElement(
suite_node,
"testcase",
name=c.name,
file="",
line="",
status="run",
result=c.result,
)
tree = ET.ElementTree(root_suites)
tree.write("filename.xml")
Проверяем структуру в процессе работы
Если хотите смотреть на дерево по мере сборки, печатайте промежуточные структуры. Вызывайте testsuites.dump() после создания каждого Element или SubElement, чтобы проверить атрибуты и вложенность. Используйте также print(), print(type(...)) и print(len(...)) вокруг значений, которые попадают в атрибуты. Такой быстрый «print‑дебаг» экономит время, сразу показывая, где в атрибут просочилась нестроковая величина.
Почему это важно
XML‑инструменты строго относятся к типам данных на границе сериализации. В ElementTree атрибуты — это обычные строки, без динамического приведения типов. Незначительное несоответствие, вроде int в атрибуте, может породить запутанный стек вызовов, намекающий на сбои записи выше по цепочке. Понимание того, что сериализатор отвергает целочисленный атрибут, позволяет исправить проблему в источнике и не тратить время на ложные гипотезы.
Итоги
При генерации XML с xml.etree явно приводите числовые значения атрибутов к строкам. Делайте это в каждом месте создания элементов — в ET.Element(...) и ET.SubElement(...). Если сериализация падает, сведите пример до минимума и быстро проверьте типы атрибутов с помощью печати или дампа дерева в stdout. Так конвейер XML остаётся предсказуемым, а вывод — совместимым с последующей обработкой.