我有一个可以从 XML 字符串构建自身并将自身写入 XML 字符串的对象。我想编写一个单元测试来测试通过 XML 的往返,但是我在比较两个 XML 版本时遇到了麻烦。空格和属性顺序似乎是问题所在。关于如何做到这一点的任何建议?这是在 Python 中,我使用的是 ElementTree(这并不重要,因为我只是在这个级别处理字符串中的 XML)。
10 回答
首先规范化 2 个 XML,然后您可以比较它们。我使用 lxml 使用了以下内容
obj1 = objectify.fromstring(expect)
expect = etree.tostring(obj1)
obj2 = objectify.fromstring(xml)
result = etree.tostring(obj2)
self.assertEquals(expect, result)
这是一个老问题,但是由于属性顺序,接受的 Kozyarchuk 的答案对我不起作用,并且minidom 解决方案也不能按原样工作(不知道为什么,我没有调试它)。
这就是我最终想出的:
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 文件时会有所帮助。
如果问题实际上只是空格和属性顺序,并且除了文本和元素之外没有其他结构需要担心,则可以使用标准 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、实体引用、注释、文档类型、命名空间等,您可以使用 DOM Level 3 Core 方法 isEqualNode。minidom 和 etree 都没有,但 pxdom 是一种支持它的实现:
def isEqualXML(a, b):
da, db= pxdom.parseString(a), pxdom.parseString(a)
return da.isEqualNode(db)
(如果您需要指定实体引用和 CDATA 部分是否与其替换的等效项匹配,您可能需要更改解析中的一些 DOMConfiguration 选项。)
一种稍微迂回的方法是解析,然后重新序列化为规范形式并进行字符串比较。pxdom 再次支持 DOM Level 3 LS 选项“canonical-form”,您可以使用它来执行此操作;使用 stdlib 的 minidom 实现的另一种方法是使用 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
使用xmldiff,这是一个 python 工具,可以找出两个相似 XML 文件之间的差异,与 diff 的方法相同。
为什么要检查 XML 数据?
测试对象序列化的方法是创建对象的实例,序列化,反序列化成新的对象,比较两个对象。当您进行破坏序列化或反序列化的更改时,此测试将失败。
检查 XML 数据的唯一事情是,如果您的序列化器发出了反序列化器所需的超集,并且反序列化器会默默地忽略它不期望的东西。
当然,如果其他东西会消耗序列化的数据,那就是另一回事了。但在这种情况下,您应该考虑为 XML 建立一个模式并对其进行验证。
我也有这个问题,今天做了一些挖掘。该doctestcompare
方法可能就足够了,但我通过Ian Bicking发现它基于formencode.doctest_xml_compare
. 现在似乎在这里。正如你所看到的,这是一个非常简单的函数,不像doctestcompare
(虽然我猜doctestcompare
是收集所有的失败,也许是更复杂的检查)。无论如何复制/导入可能是一个很好的解决方案xml_compare
。formencode
Java 组件dbUnit
进行了大量的 XML 比较,因此您可能会发现查看他们的方法很有用(尤其是发现他们可能已经解决的任何问题)。
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)