Сравнение XML в модульном тесте на Python

У меня есть объект, который может построить себя из строки XML и записать себя в строку XML. Я хотел бы написать модульный тест, чтобы проверить циклическое переключение через XML, но у меня возникают проблемы со сравнением двух версий XML. Пробелы и порядок атрибутов кажутся проблемами. Есть предложения, как это сделать? Это на Python, и я использую ElementTree (здесь это не имеет особого значения, поскольку на этом уровне я просто имею дело с XML в строках).

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
38
0
15 649
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Используйте xmldiff, инструмент Python, который определяет различия между двумя похожими XML-файлами, так же, как это делает diff.

xmldiff - это GPL. Означает ли это, что я должен открыть исходный код своего скрипта?

guettli 09.06.2017 17:16
Ответ принят как подходящий

Сначала нормализуйте 2 XML, затем вы можете их сравнить. Я использовал следующее, используя lxml

obj1 = objectify.fromstring(expect)
expect = etree.tostring(obj1)
obj2 = objectify.fromstring(xml)
result = etree.tostring(obj2)
self.assertEquals(expect, result)

Ой, я пробовал это и думал, что атрибуты упорядочены по-другому, но я посмотрел еще раз, и на самом деле мне просто не хватало одного в моем выводе. Спасибо, что ударил меня по голове.

Adam Endicott 26.11.2008 23:03

Хех. Небольшое примечание предостережения, etree не документирует никаких гарантий сериализации атрибутов в каком-либо конкретном порядке. По крайней мере, текущая реализация ElementTree на чистом Python действительно выполняет для них sort (), но неясно, можно ли полагаться на это и дальше.

bobince 27.11.2008 02:19

Мой опыт работы с etree показывает, что он сериализует их в том же порядке, в котором они были записаны в документ изначально.

Mark E. Haase 14.09.2012 23:02

Осторожно: сериализация может различаться в зависимости от версии Python, особенно порядок атрибутов.

Stan 10.06.2013 20:41

Остерегайтесь также интервалов, которые могут быть сохранены. Пример: t=ET.tostring; f=ET.fromstring; t(f('<A><B/></A>')) != t(f('<A> <B/></A>')). Проголосовали против, потому что я просто выстрелил себе в ногу из-за этого.

kjaquier 19.09.2016 18:57

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

def isEqualXML(a, b):
    da, db= minidom.parseString(a), minidom.parseString(b)
    return isEqualElement(da.documentElement, db.documentElement)

def isEqualElement(a, b):
    if a.tagName!=b.tagName:
        return False
    if sorted(a.attributes.items())!=sorted(b.attributes.items()):
        return False
    if len(a.childNodes)!=len(b.childNodes):
        return False
    for ac, bc in zip(a.childNodes, b.childNodes):
        if ac.nodeType!=bc.nodeType:
            return False
        if ac.nodeType==ac.TEXT_NODE and ac.data!=bc.data:
            return False
        if ac.nodeType==ac.ELEMENT_NODE and not isEqualElement(ac, bc):
            return False
    return True

Если вам нужно более тщательное сравнение эквивалентности, охватывающее возможности других типов узлов, включая CDATA, PI, ссылки на сущности, комментарии, типы документов, пространства имен и т. д., Вы можете использовать метод isEqualNode ядра DOM уровня 3. Ни у minidom, ни у etree этого нет, но pxdom - это одна из реализаций, которая его поддерживает:

def isEqualXML(a, b):
    da, db= pxdom.parseString(a), pxdom.parseString(a)
    return da.isEqualNode(db)

(Вы можете изменить некоторые параметры DOMConfiguration при синтаксическом анализе, если вам нужно указать, соответствуют ли ссылки на сущности и разделы CDATA их замененным эквивалентам.)

Несколько более окольный способ сделать это - проанализировать, затем повторно сериализовать до канонической формы и выполнить сравнение строк. Опять же, pxdom поддерживает опцию «canonical-form» DOM Level 3 LS, которую вы можете использовать для этого; альтернативный способ использования минидома stdlib - использовать c14n. Однако для этого у вас должны быть установлены расширения PyXML, поэтому вы по-прежнему не можете сделать это в stdlib:

from xml.dom.ext import c14n

def isEqualXML(a, b):
    da, bd= minidom.parseString(a), minidom.parseString(b)
    a, b= c14n.Canonicalize(da), c14n.Canonicalize(db)
    return a==b

Зачем вы вообще изучаете XML-данные?

Способ тестирования сериализации объекта - создать экземпляр объекта, сериализовать его, десериализовать в новый объект и сравнить два объекта. Когда вы вносите изменение, которое нарушает сериализацию или десериализацию, этот тест завершится ошибкой.

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

Конечно, если что-то еще будет потреблять сериализованные данные, это другое дело. Но в этом случае вам следует подумать о создании схемы для XML и ее проверке.

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

Adam Endicott 27.11.2008 00:32

Компонент Java dbUnit выполняет множество сравнений XML, поэтому вам может быть полезно взглянуть на их подход (особенно, чтобы найти любые ошибки, которые они, возможно, уже рассмотрели).

Это старый вопрос, но принятый Ответ Козярчука не работает для меня из-за порядка атрибутов, а раствор минидома тоже не работает как есть (не знаю почему, я не отлаживал его).

Вот что я наконец придумал:

from doctest import Example
from lxml.doctestcompare import LXMLOutputChecker

class XmlTest(TestCase):
    def assertXmlEqual(self, got, want):
        checker = LXMLOutputChecker()
        if not checker.check_output(want, got, 0):
            message = checker.output_difference(Example("", want), got, 0)
            raise AssertionError(message)

Это также дает различие, которое может быть полезно в случае больших файлов xml.

У меня проблемы с тем, чтобы заставить это работать в Python3 из-за проблем с кодировкой строк, независимо от того, какую комбинацию bytes(), bytearray() или encode('utf-8') я использовал. Я не уверен, проблема ли это в библиотеке или я просто что-то упускаю, но у меня это не сработало.

Aaron D 03.10.2017 15:57

Я не уверен, в чем проблема; этот подход используется здесь как для тестов Python 2, так и Python 3: github.com/scrapinghub/webstruct/blob/…

Mikhail Korobov 03.10.2017 17:05

У меня тоже была эта проблема, и я немного покопался в ней сегодня. doctestcompare подход может хватить, но я обнаружил через Ян Бикинг, что он основан на formencode.doctest_xml_compare. Кажется, теперь это здесь. Как видите, это довольно простая функция, в отличие от doctestcompare (хотя я предполагаю, что doctestcompare собирает все сбои и, возможно, более сложную проверку). В любом случае копирование / импорт xml_compare из formencode может быть хорошим решением.

def xml_to_json(self, xml):
    """Receive 1 lxml etree object and return a json string"""
    def recursive_dict(element):
        return (element.tag.split('}')[1],
                dict(map(recursive_dict, element.getchildren()),
                     **element.attrib))
    return json.dumps(dict([recursive_dict(xml)]),
                      default=lambda x: str(x))

def assertEqualXML(self, xml_real, xml_expected):
    """Receive 2 objectify objects and show a diff assert if exists."""
    xml_expected_str = json.loads(self.xml_to_json(xml_expected))
    xml_real_str = json.loads(self.xml_to_json(xml_real))
    self.maxDiff = None
    self.assertEqual(xml_real_str, xml_expected_str)

Вы могли увидеть такой вывод:

                u'date': u'2016-11-22T19:55:02',
                u'item2': u'MX-INV0007',
         -      u'item3': u'Payments',
         ?                  ^^^
         +      u'item3': u'OAYments',
         ?                  ^^^ +

Это легко сделать с помощью minidom:

class XmlTest(TestCase):
    def assertXmlEqual(self, got, want):
        return self.assertEqual(parseString(got).toxml(), parseString(want).toxml())

Раствор Стевойсяка

в моем случае не работает для python3. Зафиксированный:

from lxml.doctestcompare import LXMLOutputChecker, PARSE_XML

class XmlTest(TestCase):
def assertXmlEqual(self, got, want):
    checker = LXMLOutputChecker()
    if not checker.check_output(want.encode(), got.encode(), PARSE_XML):
        message = checker.output_difference(Example(b"", want.encode()), got.encode(), PARSE_XML)
        raise AssertionError(message)

Другие вопросы по теме