45

I have an object that can build itself from an XML string, and write itself out to an XML string. I'd like to write a unit test to test round tripping through XML, but I'm having trouble comparing the two XML versions. Whitespace and attribute order seem to be the issues. Any suggestions for how to do this? This is in Python, and I'm using ElementTree (not that that really matters here since I'm just dealing with XML in strings at this level).

10 Answers 10

20

This is an old question, but the accepted Kozyarchuk's answer doesn't work for me because of attributes order, and the minidom solution doesn't work as-is either (no idea why, I haven't debugged it).

This is what I finally came up with:

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) 

This also produces a diff that can be helpful in case of large xml files.

Sign up to request clarification or add additional context in comments.

4 Comments

I have problems getting this to work in Python3, due to string encoding issues, no matter which combination of bytes(), bytearray() or encode('utf-8') I used. I'm not sure if that's a problem in the library or if I'm just missing something but this didn't work for me.
I'm not sure what's the issue; this approach is used both for Python 2 and Python 3 tests here: github.com/scrapinghub/webstruct/blob/…
I have stepped through the code and doctestcompare.py of lxml package isn't properly ported to python3 yet. self.get_parser() requires strings and then the same function requires bytes later down.
Really helpful answer for comparing HTML with lxml!! Thanks.
19

First normalize 2 XML, then you can compare them. I've used the following using lxml

import xml.etree.ElementTree as ET obj1 = objectify.fromstring(expect) expect = ET.tostring(obj1) obj2 = objectify.fromstring(xml) result = ET.tostring(obj2) self.assertEquals(expect, result) 

5 Comments

Oh man, I had tried this and thought the attributes were ordered differently, but I looked again and I was actually just missing one in my output. Thanks for hitting me over the head.
Heh. Slight note of caution, etree does not document any guarantee to serialise attributes in any particular order. At least the current pure-Python implementation of ElementTree does do a sort() on them, but it's not clear you can rely on this remaining so.
My experience with etree is that it serializes them in the same order they were written to the document originally.
Beware : the serialization may vary with the version of Python, especially the attribute order.
Beware of the spacing as well that may be conserved. Example: t=ET.tostring; f=ET.fromstring; t(f('<A><B/></A>')) != t(f('<A> <B/></A>')). Downvoted, because I just shot myself in the foot with that.
7

If the problem is really just the whitespace and attribute order, and you have no other constructs than text and elements to worry about, you can parse the strings using a standard XML parser and compare the nodes manually. Here's an example using minidom, but you could write the same in etree pretty simply:

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 

If you need a more thorough equivalence comparison, covering the possibilities of other types of nodes including CDATA, PIs, entity references, comments, doctypes, namespaces and so on, you could use the DOM Level 3 Core method isEqualNode. Neither minidom nor etree have that, but pxdom is one implementation that supports it:

def isEqualXML(a, b): da, db= pxdom.parseString(a), pxdom.parseString(a) return da.isEqualNode(db) 

(You may want to change some of the DOMConfiguration options on the parse if you need to specify whether entity references and CDATA sections match their replaced equivalents.)

A slightly more roundabout way of doing it would be to parse, then re-serialise to canonical form and do a string comparison. Again pxdom supports the DOM Level 3 LS option ‘canonical-form’ which you could use to do this; an alternative way using the stdlib's minidom implementation is to use c14n. However you must have the PyXML extensions install for this so you still can't quite do it within the 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 

Comments

5

Use xmldiff, a python tool that figures out the differences between two similar XML files, the same way that diff does it.

1 Comment

xmldiff is GPL. Does this mean, I have to open source my script?
4

Why are you examining the XML data at all?

The way to test object serialization is to create an instance of the object, serialize it, deserialize it into a new object, and compare the two objects. When you make a change that breaks serialization or deserialization, this test will fail.

The only thing checking the XML data is going to find for you is if your serializer is emitting a superset of what the deserializer requires, and the deserializer silently ignores stuff it doesn't expect.

Of course, if something else is going to be consuming the serialized data, that's another matter. But in that case, you ought to be thinking about establishing a schema for the XML and validating it.

1 Comment

Yes, something else is going to be consuming the serialized data. I may get to the point of building a schema and validating it, but for now doing a string comparison of the XML is good enough.
1

I also had this problem and did some digging around it today. The doctestcompare approach may suffice, but I found via Ian Bicking that it is based on formencode.doctest_xml_compare. Which appears to now be here. As you can see that is a pretty simple function, unlike doctestcompare (although I guess doctestcompare is collecting all the failures and maybe more sophisticated checking). Anyway copying/importing xml_compare out of formencode may be a good solution.

Comments

1

Stevoisiak's solution

in my case doesn't work for python3. Fixed:

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) 

Comments

0

The Java component dbUnit does a lot of XML comparisons, so you might find it useful to look at their approach (especially to find any gotchas that they may have already addressed).

Comments

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

You could see a output like as:

 u'date': u'2016-11-22T19:55:02', u'item2': u'MX-INV0007', - u'item3': u'Payments', ? ^^^ + u'item3': u'OAYments', ? ^^^ + 

Comments

0

It can be easily done with minidom:

class XmlTest(TestCase): def assertXmlEqual(self, got, want): return self.assertEqual(parseString(got).toxml(), parseString(want).toxml()) 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.