Confronto di XML in un unit test in Python
-
11-07-2019 - |
Domanda
Ho un oggetto che può costruirsi da una stringa XML e scriverlo su una stringa XML. Mi piacerebbe scrivere un test unitario per testare l'attraversamento circolare in XML, ma ho problemi a confrontare le due versioni XML. Gli spazi bianchi e l'ordine degli attributi sembrano essere i problemi. Qualche suggerimento su come farlo? Questo è in Python e sto usando ElementTree (non è quello che conta davvero qui dato che ho a che fare con XML nelle stringhe a questo livello).
Soluzione
Prima normalizza 2 XML, quindi puoi confrontarli. Ho usato quanto segue usando lxml
obj1 = objectify.fromstring(expect)
expect = etree.tostring(obj1)
obj2 = objectify.fromstring(xml)
result = etree.tostring(obj2)
self.assertEquals(expect, result)
Altri suggerimenti
Questa è una vecchia domanda, ma la risposta di Kozyarchuk accettata non funziona per me a causa dell'ordine degli attributi, e la soluzione minidom non funziona così com'è (non ho idea del perché, non ho eseguito il debug).
Questo è quello che alla fine mi è venuta in mente:
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)
Questo produce anche un diff che può essere utile in caso di file XML di grandi dimensioni.
Se il problema riguarda davvero solo gli spazi bianchi e l'ordine degli attributi e non ci sono altri costrutti oltre al testo e agli elementi di cui preoccuparsi, è possibile analizzare le stringhe utilizzando un parser XML standard e confrontare manualmente i nodi. Ecco un esempio usando minidom, ma potresti scrivere lo stesso in etree semplicemente:
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
Se hai bisogno di un confronto di equivalenza più approfondito, che copra le possibilità di altri tipi di nodi tra cui CDATA, PI, riferimenti a entità, commenti, doctype, spazi dei nomi e così via, puoi usare il metodo Core Level 3 isEqualNode. Né minidom né etree lo hanno, ma pxdom è un'implementazione che lo supporta:
def isEqualXML(a, b):
da, db= pxdom.parseString(a), pxdom.parseString(a)
return da.isEqualNode(db)
(Potresti voler modificare alcune delle opzioni di DOMConfiguration all'analisi se devi specificare se i riferimenti di entità e le sezioni CDATA corrispondono ai loro equivalenti sostituiti.)
Un modo leggermente più circolare di farlo sarebbe quello di analizzare, quindi ricerializzare in forma canonica e fare un confronto di stringhe. Ancora una volta pxdom supporta l'opzione LS livello DOM 3 & # 8216; forma canonica & # 8217; che potresti usare per fare questo; un modo alternativo di usare l'implementazione minidom di stdlib è usare c14n. Tuttavia è necessario disporre delle estensioni PyXML installate per questo, quindi non è ancora possibile farlo all'interno dello 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
Usa xmldiff , uno strumento python che identifica le differenze tra due file XML simili, allo stesso modo che diff lo fa.
Perché stai esaminando i dati XML?
Il modo per testare la serializzazione degli oggetti è creare un'istanza dell'oggetto, serializzarlo, deserializzarlo in un nuovo oggetto e confrontare i due oggetti. Quando si effettua una modifica che interrompe la serializzazione o la deserializzazione, questo test fallirà.
L'unica cosa che verifica i dati XML sta per trovare per te è se il tuo serializzatore sta emettendo un superset di ciò che il deserializzatore richiede e il deserializzatore ignora silenziosamente ciò che non si aspetta.
Ovviamente, se qualcos'altro consumerà i dati serializzati, è un'altra questione. Ma in quel caso, dovresti pensare di stabilire uno schema per l'XML e convalidarlo.
Ho anche avuto questo problema e ho fatto qualche ricerca intorno ad esso oggi. L'approccio doctestcompare
può essere sufficiente, ma ho scoperto tramite Ian Bicking che si basa su formencode.doctest_xml_compare
. Che ora sembra essere qui . Come puoi vedere, questa è una funzione abbastanza semplice, a differenza di doctestcompare
(anche se immagino che doctestcompare
stia raccogliendo tutti gli errori e magari un controllo più sofisticato). In ogni caso, copiare / importare xml_compare
da formencode
può essere una buona soluzione.
Il componente Java dbUnit
fa molti confronti XML, quindi potresti trovare utile esaminare il loro approccio (specialmente per trovare i gotcha che potrebbero aver già affrontato).
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)
Potresti vedere un output come:
u'date': u'2016-11-22T19:55:02',
u'item2': u'MX-INV0007',
- u'item3': u'Payments',
? ^^^
+ u'item3': u'OAYments',
? ^^^ +
Può essere facilmente eseguito con minidom
:
class XmlTest(TestCase):
def assertXmlEqual(self, got, want):
return self.assertEqual(parseString(got).toxml(), parseString(want).toxml())