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).

32
demandé sur Adam Endicott 2008-11-26 22:09:08

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)
13
répondu Kozyarchuk 2017-11-06 23:28:05

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.

15
répondu Mikhail Korobov 2017-11-06 23:23:13

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
7
répondu bobince 2008-11-26 19:56:19

utiliser xmldiff , un outil python qui calcule les différences entre deux fichiers XML similaires, de la même manière que diff le fait.

5
répondu andrewrk 2008-11-26 19:19:11

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.

2
répondu Robert Rossney 2008-11-26 20:46:45

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.

1
répondu pfctdayelise 2017-05-23 12:30:52

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).

0
répondu Rob Williams 2008-11-27 00:20:52
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',
         ?                  ^^^ +
0
répondu moylop260 2016-11-23 02:05:34

il peut être facilement fait avec minidom :

class XmlTest(TestCase):
    def assertXmlEqual(self, got, want):
        return self.assertEqual(parseString(got).toxml(), parseString(want).toxml())
0
répondu porton 2018-09-27 15:15:07