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 остаётся предсказуемым, а вывод — совместимым с последующей обработкой.

Статья основана на вопросе на StackOverflow от Harry и ответе furas.