python-xmlunittest icon indicating copy to clipboard operation
python-xmlunittest copied to clipboard

How do I assert that element doesn't exist or has no children?

Open x-yuri opened this issue 6 years ago • 2 comments

It doesn't seem to be possible right now.

Possible workaround:

    def assertXpathExists(self, node, xpath):
        if self.eval_xpath(node, 'count(' + xpath + ')') == 0:
            self.fail('''xpath "%s" doesn't exist''' % xpath)

    def assertXpathNotExists(self, node, xpath):
        if self.eval_xpath(node, 'count(' + xpath + ')') > 0:
            self.fail('''xpath "%s" exists''' % xpath)

    def eval_xpath(self, node, xpath):
        expression = self.build_xpath_expression(node, xpath)
        try:
            return expression.evaluate(node)
        except etree.XPathEvalError as error:
            self.fail_xpath_error(node, expression.path, error)

UPD I don't mean to offend you, but I ended up writing a bunch of my own helpers. The only thing I'm still using so far is assertXmlValidDTD(). They may be far from being perfect, but with them, my tests are much more readable. And they don't suffer from the flaw (the way I see it) that e.g. assertXpathValues() passes when the target element doesn't exist.

Leaving them here, for what it's worth:

from django.template import Template, RequestContext
from lxml import etree

import xml.dom.minidom as mdom

class XmlTestMixin:
    # assertXML + can compare not the whole document but a subtree
    # + can ignore subtrees
    # _remove_doctype is a temporary fix
    # assertXMLEqual bails out for documents containing doctype
    # they have fixed it in the master
    # https://code.djangoproject.com/ticket/30497
    def assertXML(self, expected, actual, **kwargs):
        if 'subtree' in kwargs:
            actual = self._extract_subtree(actual, kwargs['subtree'])
        if 'ignore' in kwargs:
            actual = self._ignore_xpaths(actual, kwargs['ignore'])
        self.assertXMLEqual(
            self._remove_doctype(expected),
            self._remove_doctype(actual))

    def _extract_subtree(self, xml, xpath):
        t = etree.fromstring(xml.encode()).getroottree()
        return etree.tostring(
            self.xpath(t, xpath)[0],
            encoding=t.docinfo.encoding
        ).decode()

    def _ignore_xpaths(self, xml, xpaths):
        t = etree.fromstring(xml.encode()).getroottree()
        for xp in xpaths:
            for el in t.xpath(xp):
                el.getparent().remove(el)
        kwargs = {'encoding': t.docinfo.encoding, **(
            {'xml_declaration': True}
            if self._has_xml_declaration(xml)
            else {}
        )}
        return etree.tostring(t, **kwargs).decode()

    def _remove_doctype(self, xml):
        dom = mdom.parseString(xml)
        if not dom.version:  # no xml declaration
            return xml
        for n in dom.childNodes:
            if n.nodeType == mdom.Node.DOCUMENT_TYPE_NODE:
                dom.removeChild(n)
        return dom.toxml()

    def _has_xml_declaration(self, xml):
        return mdom.parseString(xml).version

    # I usually start with a test that handles the general case
    # (compare the whole document + ignore subtrees or compare a subtree)
    # that's where Django templates come in handy
    # succeeding tests deal with the details
    def render_template(self, template, context, response):
        return Template(template) \
            .render(RequestContext(response.wsgi_request, context))


    def assertElementExists(self, node, xpath):
        if self.xpath(node, 'count(' + xpath + ')') == 0:
            self.fail('''Element doesn't exist (%s)''' % xpath)

    def assertElementNotExists(self, node, xpath):
        if self.xpath(node, 'count(' + xpath + ')') > 0:
            self.fail('Element exists (%s)' % xpath)

    def assertElementText(self, root, xpath, text):
        r = self.xpath(root, xpath)
        self._assertOneElement(xpath, r)
        if self._normalize_space(r[0].text) != text:
            self.fail('''Element's text differs (%s)\n'''
                      'Expected: %s\n'
                      'Actual: %s\n'
                      % (xpath, text, r[0].text))

    def assertElementsText(self, root, xpath, texts):
        r = self.xpath(root, xpath)
        if len(r) != len(texts):
            self.fail('''Number of elements doesn't match'''
                      'that of the expected values (%s)'
                      % xpath)
        actual_texts = set(self._normalize_space(e.text) for e in r)
        if actual_texts != texts:
            self.fail('''Elements' texts differ (%s)\n'''
                      'Expected: %s\n'
                      'Actual: %s\n'
                      % (xpath, texts, actual_texts))

    def assertAttributeValue(self, root, xpath, attr, value):
        r = self.xpath(root, xpath)
        self._assertOneElement(xpath, r)
        if attr not in r[0].attrib:
            self.fail('''Attribute doesn't exist (%s)''' % xpath)
        if r[0].attrib[attr] != value:
            self.fail('Attribute value differs (%s)\n'
                      'Expected: %s\n'
                      'Actual: %s\n'
                      % (xpath, value, r[0].attrib[attr]))
        
    def _assertOneElement(self, xpath, r):
        if len(r) == 0:
            self.fail('xpath "%s" not found' % xpath)
        elif len(r) > 1:
            self.fail('xpath "%s" resolves to multiple elements' % xpath)

    def xpath(self, xml, xpath):
        tree = etree.fromstring(xml.encode()).getroottree() \
            if isinstance(xml, str) \
            else xml
        return tree.xpath(xpath)

    def _normalize_space(self, s):
        return " ".join(s.split())

x-yuri avatar May 23 '19 14:05 x-yuri

Hi! I was on vacations for the last few days. I'll have a look this week.

Thanks for sharing that! I believe this library could be updated with new ideas, so they are very welcome!

Exirel avatar May 26 '19 20:05 Exirel

Good to know you took it positively. I've commented the code a bit.

To be frank, I'm still learning how to write tests. So I'm not sure about my helpers. Especially, about the part from assertXML() up to render_template(). So let me elaborate on this. I need XML to generate a price list of an online store that goes like this:

<?xml version="1.0" encoding="UTF-8" ?>
<store>
  <name>...</name>
  ...
  <categories>...</categories>
  <goods>...</goods>
</store>

And my tests are basically split into 3 groups (store itself, categories, goods). Each group starts with a test that handles the general case, ignoring some subtrees, or focusing on them (ignoring everything else). Later tests handle the details. And that's why I need Django templates (render_template()). I could probably use fixed data in the "general case" tests and get away without Django templates. But I decided not to.

x-yuri avatar May 26 '19 21:05 x-yuri