Comparer XML dans un test unitaire en Python
j'ai un objet qui peut se construire à partir d'une chaîne XML, et s'écrire à une chaîne XML. J'aimerais faire un test unitaire pour tester le routage à travers XML, mais j'ai du mal à comparer les deux versions XML. Les espaces et l'ordre des attributs semblent être les problèmes. Des suggestions pour faire cela? C'est en Python, et j'utilise ElementTree (ce qui n'a pas vraiment d'importance ici puisque je m'occupe juste de XML dans les chaînes à ce niveau).
9 réponses
d'abord normaliser 2 XML, puis vous pouvez les comparer. J'ai utilisé ce qui suit en utilisant lxml
obj1 = objectify.fromstring(expect)
expect = etree.tostring(obj1)
obj2 = objectify.fromstring(xml)
result = etree.tostring(obj2)
self.assertEquals(expect, result)
C'est une vieille question, mais la réponse acceptée de Kozyarchuk ne fonctionne pas pour moi à cause de l'ordre des attributs, et la minidom solution ne fonctionne pas comme-est non plus (aucune idée pourquoi, je ne l'ai pas débogué).
C'est ce que j'ai finalement trouvé:
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)
cela produit aussi une différence qui peut être utile dans le cas de grands fichiers xml.
si le problème est vraiment juste l'ordre des espaces et des attributs, et que vous n'avez pas d'Autres constructions que du texte et des éléments à vous soucier, vous pouvez analyser les chaînes en utilisant un analyseur XML standard et comparer les noeuds manuellement. Voici un exemple utilisant minidom, mais vous pourriez écrire la même chose dans etree tout simplement:
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
si vous avez besoin d'une comparaison d'équivalence plus approfondie, couvrant les possibilités d'autres types de noeuds y compris CDATA, PIs, entity références, commentaires, doctypes, namespaces, etc., vous pouvez utiliser la méthode de base DOM de niveau 3 isEqualNode. Ni minidom ni etree n'ont cela, mais pxdom est une implémentation qui le supporte:
def isEqualXML(a, b):
da, db= pxdom.parseString(a), pxdom.parseString(a)
return da.isEqualNode(db)
(vous pouvez modifier certaines des options de DOMConfiguration sur le parse si vous avez besoin de préciser si les références d'entité et les sections CDATA correspondent à leurs équivalents remplacés.)
une façon un peu plus détournée de le faire serait de parse, puis re-serialiser à la forme canonique et faire une comparaison de chaîne. Encore une fois, pxdom supporte L'option DOM Level 3 LS ‘canonical-form’ que vous pouvez utiliser pour ce faire; une autre façon d'utiliser l'implémentation minidom de stdlib est d'utiliser c14n. Cependant, vous devez avoir les extensions PyXML à installer pour cela, donc vous ne pouvez toujours pas tout à fait le faire dans le 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
pourquoi examinez-vous les données XML?
Le moyen de tester la sérialisation d'un objet consiste à créer une instance de l'objet, de le sérialiser, désérialiser dans un nouvel objet, et de comparer les deux objets. Lorsque vous effectuez un changement qui casse la sérialisation ou la deserialisation, ce test échouera.
la seule chose que le contrôle des données XML va trouver pour vous est si votre serializer émet un super-ensemble de ce que le deserializer exige, et le désérialiseur ignore silencieusement des choses auxquelles il ne s'attend pas.
bien sûr, si quelque chose d'autre va consommer les données sérialisées, c'est une autre affaire. Mais dans ce cas, vous devriez penser à établir un schéma pour le XML et le valider.
j'ai aussi eu ce problème et j'ai fait quelques recherches autour aujourd'hui. Le doctestcompare
approche peut suffire, mais j'ai trouvé via Ian Bicking qu'il est basé sur formencode.doctest_xml_compare
. Qui semble être maintenant ici . Comme vous pouvez le voir que c'est une fonction assez simple, contrairement doctestcompare
(bien que je suppose que doctestcompare
est la collecte de tous les échecs et peut-être vérification plus sophistiquée). De toute façon copie/importation xml_compare
de formencode
peut être une bonne solution.
le composant Java dbUnit
fait beaucoup de comparaisons XML, de sorte que vous pourriez trouver utile d'examiner leur approche (en particulier pour trouver les gotchas qu'ils ont peut-être déjà abordés).
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)
vous pourriez voir une sortie comme:
u'date': u'2016-11-22T19:55:02',
u'item2': u'MX-INV0007',
- u'item3': u'Payments',
? ^^^
+ u'item3': u'OAYments',
? ^^^ +
il peut être facilement fait avec minidom
:
class XmlTest(TestCase):
def assertXmlEqual(self, got, want):
return self.assertEqual(parseString(got).toxml(), parseString(want).toxml())