XML 文字列からそれ自体を構築し、それ自体を XML 文字列に書き出すことができるオブジェクトがあります。XML を介したラウンド トリップをテストする単体テストを作成したいのですが、2 つの 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、エンティティ参照、コメント、Doctype、名前空間などを含む他のタイプのノードの可能性をカバーする、より完全な同等性比較が必要な場合は、DOM レベル 3 コア メソッド isEqualNode を使用できます。minidom にも etree にもありませんが、pxdom はそれをサポートする実装の 1 つです。
def isEqualXML(a, b):
da, db= pxdom.parseString(a), pxdom.parseString(a)
return da.isEqualNode(db)
(エンティティ参照と CDATA セクションが置き換えられた同等のものと一致するかどうかを指定する必要がある場合は、パースで DOMConfiguration オプションの一部を変更することをお勧めします。)
それを行うためのもう少し遠回りな方法は、解析してから正規の形式に再シリアル化し、文字列比較を行うことです。ここでも pxdom は、これを行うために使用できる DOM レベル 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を使用します。これは、2 つの類似した XML ファイルの違いを diff と同じ方法で把握する Python ツールです。
XML データを調べているのはなぜですか。
オブジェクトのシリアル化をテストする方法は、オブジェクトのインスタンスを作成し、それをシリアル化し、それを新しいオブジェクトに逆シリアル化し、2 つのオブジェクトを比較することです。シリアル化または逆シリアル化を中断する変更を行うと、このテストは失敗します。
XML データをチェックするだけでわかることは、シリアライザーがデシリアライザーが必要とするもののスーパーセットを発行しているかどうかであり、デシリアライザーは予期しないものを静かに無視します。
もちろん、シリアル化されたデータを他の何かが消費する場合、それは別の問題です。しかしその場合、XML のスキーマを確立して検証することを検討する必要があります。
私もこの問題を抱えていて、今日それを掘り下げました。このdoctestcompare
アプローチで十分かもしれませんが、Ian Bickingを介して、に基づいていることがわかりましたformencode.doctest_xml_compare
。これは今ここにあるようです。ご覧のとおり、これは非常に単純な関数です(ただし、すべての障害を収集し、より高度なチェックを行うとdoctestcompare
思います)。doctestcompare
とにかく、からのコピー/インポートxml_compare
はformencode
良い解決策かもしれません。
JavaコンポーネントdbUnit
は多くのXML比較を行うので、それらのアプローチを調べると役立つ場合があります(特に、Javaコンポーネントがすでに対処している可能性のある落とし穴を見つけるため)。
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',
? ^^^ +