跳转到主要内容

用于检查HTML解析树部分的匹配器

项目描述

这是一个库,用于使编写HTML内容的测试变得简单和健壮。

完成这项工作的简单方法可能是断言您的HTML包含字符串

>>> html = ('<a href="https://launchpad.net/testtools" '
...     'class="awesome">testtools <b>rocks</b></a>')

这很容易在您添加一个与测试无关的CSS类或您的模板库更改以按字母顺序排序属性等小改动时出错。

显然,在解析树上工作会更好,BeautifulSoup就是其中的一部分。

BeautifulSoup

>>> import bs4
>>> root = bs4.BeautifulSoup(html, "html.parser")

这是一个HTML解析库,包括在文档中搜索匹配标签的方法。如果您有您文档的解析表示形式,您可以通过以下方式找到上面的部分:

>>> import re
>>> anchor_tags = root.find_all(
...    "a", attrs={"href": "https://launchpad.net/testtools",
...        "class": "awesome"})
>>> print(anchor_tags)
[<a class="awesome" href="https://launchpad.net/testtools">testtools <b>rocks</b></a>]

这将返回一个列表,假设只有一个条目,即bs4.Tag类型的<a>标签。您可以使用以下方法定位嵌套标签:

>>> anchor_tag = anchor_tags[0]
>>> anchor_tag.find_all("b")
[<b>rocks</b>]

这将再次返回一个单项列表。

虽然这有助于更稳健地识别文档的各个部分,但它并不适用于测试。为此,我们需要一些方法来检查文档是否符合规范。

匹配器

这正是testtools的优势所在。我们不需要再提供一个TestCase子类,然后将其整合到您的测试类层次结构中,我们只需定义一组testtools.Matcher类。

如果您使用testtools,则可以轻松地在测试中使用这些类,使用assertThat。如果没有,它们有一个简单且易于在测试类中使用的高级接口。

让我们来演示。

首先,我们将展示如何创建一个匹配器,以检查我们的文档是否至少包含一个指向testtools Launchpad页面的链接,并且这个链接具有特定的css类,并且在锚文本中提到testtools。

>>> import soupmatchers
>>> print(soupmatchers.Tag(
...     "link to testtols", "a",
...     attrs={"href": "https://launchpad.net/testtools",
...         "class": "awesome"}))
Tag("link to testtols",
<a class='awesome' href='https://launchpad.net/testtools' ...>...</a>)

这可能看起来有些熟悉。

请注意,soupmatchers.Tag对象的文本表示并不是将进行字面匹配的内容,它只是试图表达将要匹配的内容。

此外,soupmatchers还允许您指定标签必须包含以匹配的文本。

>>> print(soupmatchers.Tag(
...     "link to testtools", "a",
...     attrs={"href": "https://launchpad.net/testtools",
...            "class": "awesome"}, text=re.compile(r"testtools")))
Tag("link to testtools",
<a class='awesome' href='https://launchpad.net/testtools'
...>re.compile('testtools') ...</a>)

现在,让我们定义一个匹配器,以匹配上面的粗体标签。

>>> print(soupmatchers.Tag("bold rocks", "b", text="rocks"))
Tag("bold rocks", <b ...>rocks ...</b>)

显然,这允许粗体标签位于锚标签之外,但不用担心,我们可以创建一个匹配器来检查一个标签是否位于另一个标签内部,只需简单地使用Within匹配器来组合这两个。

>>> print(soupmatchers.Within(
...     soupmatchers.Tag(
...         "link to testtools", "a",
...         attrs={"href": "https://launchpad.net/testtools",
...                "class": "awesome"}, text=re.compile(r"testtools")),
...     soupmatchers.Tag("bold rocks", "b", text="rocks")))
Tag("bold rocks", <b ...>rocks ...</b>) within Tag("link to testtools",
<a class='awesome' href='https://launchpad.net/testtools'
...>re.compile('testtools') ...</a>)

这意味着第一个匹配器仅在第二个匹配器匹配解析树的根节点时才会匹配。

这些匹配器在解析表示上工作,但这并不意味着每次使用它们时都必须进行解析。为了简化这一点,您可以使用以下方法:

>>> print(soupmatchers.HTMLContains(
...     soupmatchers.Tag("some image", "image")))
HTML contains [Tag("some image", <image ...>...</image>)]

创建一个匹配器,在检查标签之前先解析字符串。

考虑到您经常需要检查HTML中的多个内容,您可以将多个soupmatchers.Tag对象传递给soupmatchers.HTMLContains的构造函数,并且生成的匹配器只有在所有传递的匹配器都匹配时才会匹配。

使用匹配器

但这还没有解释如何使用匹配器对象,为此,您需要使用它们的match()方法。

>>> import testtools
>>> matcher = testtools.matchers.Equals(1)
>>> match = matcher.match(1)
>>> print(match)
None

如果匹配器匹配了您传递的内容,则返回的match将是None,否则它将是一个testtools.Mismatch对象。用unittest语言来说

match = matcher.match(content) self.assertEquals(None, match)

或者,如果您子类化了testtools.TestCase,

self.assertThat(content, matcher)

测试响应

对于那些使用具有测试响应对象的框架的人来说,您甚至可以更进一步,一次性检查整个响应。

soupmatchers.ResponseHas匹配器类将检查传入对象的response_code属性是否与预期值匹配,并检查content属性是否与您指定的任何匹配器匹配。

>>> print(soupmatchers.ResponseHas(
...     status_code=404,
...     content_matches=soupmatchers.HTMLContains(soupmatchers.Tag(
...         "an anchor", "a"))))
ResponseHas(status_code=404, content_matches=HTML contains
[Tag("an anchor", <a ...>...</a>)])

其中status_code参数默认为200。

由于处理HTML非常常见,因此有更简单的方式来编写上述代码。

>>> print(soupmatchers.HTMLResponseHas(
...     status_code=404, html_matches=soupmatchers.Tag("an anchor", "a")))
HTMLResponseHas(status_code=404, content_matches=HTML contains
[Tag("an anchor", <a ...>...</a>)])

稍后,将添加类似的对象来处理XML和JSON。

此匹配器设计用于与Django一起工作,但将适用于具有这两个属性的任何对象。

将所有这些结合起来,我们可以使用以下方法进行原始检查:

>>> class ExpectedResponse(object):
...     status_code = 200
...     content = html
>>> class UnexpectedResponse(object):
...     status_code = 200
...     content = "<h1>This is some other response<h1>"
>>> child_matcher = soupmatchers.Tag("bold rocks", "b", text="rocks")
>>> anchor_matcher = soupmatchers.Tag(
...     "testtools link", "a",
...     attrs={"href": "https://launchpad.net/testtools",
...            "class": "awesome"},
...     text=re.compile(r"testtools"))
>>> combined_matcher = soupmatchers.Within(anchor_matcher, child_matcher)
>>> response_matcher = soupmatchers.HTMLResponseHas(
...     html_matches=combined_matcher)
>>> #self.assertThat(response, response_matcher)
>>> match = response_matcher.match(ExpectedResponse())
>>> print(match)
None
>>> match = response_matcher.match(UnexpectedResponse())
>>> print(repr(match)) #doctest: +ELLIPSIS
<soupmatchers.TagMismatch object at ...>
>>> print(match.describe())
Matched 0 times
Here is some information that may be useful:
  0 matches for "bold rocks" in the document.
  0 matches for "testtools link" in the document.

这虽然冗长,但检查了很多东西,由于不依赖于特定的文本输出,因此易于维护。

检查模式匹配的次数

还记得 find_all 返回一个列表,我们只是假设示例中只找到一个标签吗? 好吧,匹配器允许你不仅仅假设这一点,它们允许你断言。这意味着你可以通过传递

count=1

在构造函数中,来断言特定的标签只出现一次。

>>> tag_matcher = soupmatchers.Tag("testtools link", "a",
...    attrs={"href": "https://launchpad.net/testtools"}, count=1)
>>> html_matcher = soupmatchers.HTMLContains(tag_matcher)
>>> content = '<a href="https://launchpad.net/testtools"></a>'
>>> match = html_matcher.match(content)
>>> print(match)
None
>>> match = html_matcher.match(content * 2)
>>> print(match.describe())
Matched 2 times
The matches were:
  <a href="https://launchpad.net/testtools"></a>
  <a href="https://launchpad.net/testtools"></a>

同样,你可以通过创建一个 soupmatchers.Tag 并设置

count=0

>>> tag_matcher = soupmatchers.Tag("testtools link", "a",
...    attrs={"href": "https://launchpad.net/testtools"}, count=0)
>>> html_matcher = soupmatchers.HTMLContains(tag_matcher)
>>> content = '<a href="https://launchpad.net/testtools"></a>'
>>> match = html_matcher.match(content)
>>> print(match.describe())
Matched 1 time
The match was:
  <a href="https://launchpad.net/testtools"></a>

来断言特定的标签不存在。

如果只想断言一个标签至少匹配给定次数或最多匹配给定次数,那么你可能需要修改代码以允许这样做。

失败信息

由于 Tag 只指定了匹配模式,当发生错误时,很难知道哪些信息对阅读输出的人来说是有用的。

>>> matcher = soupmatchers.HTMLContains(soupmatchers.Tag("bold", "b"))
>>> mismatch = matcher.match("<image></image>")
>>> print(list(mismatch.get_details().keys()))
['html']
>>> print(''.join(list(mismatch.get_details()["html"].iter_text())))
<image></image>

做一件坏事就是打印整个 HTML 文档,因为它通常很大,可能会掩盖失败信息。有时查看 HTML 是找到问题的最佳方式。因此,Mismatch 可以提供整个文档。如果你在 Mismatch 上调用 get_details(),你将获得一个包含 "html" 键的值为 HTML 的字典。

尽管如此,你仍然需要考虑在失败信息中打印什么。

如果至少有匹配项,你希望看到匹配的字符串。当匹配项太多时,这特别有用,当你期望多个匹配项,但找到的比预期的少时,了解哪些匹配可以缩小搜索范围。

>>> matcher = soupmatchers.HTMLContains(soupmatchers.Tag(
...        "no bold", "b", count=0))
>>> mismatch = matcher.match("<b>rocks</b>")
>>> print(mismatch.describe())
Matched 1 time
The match was:
    <b>rocks</b>

如果匹配项不足,失败信息将尝试告诉你关于最近似匹配项的信息,希望其中之一能给你提供解决问题的线索。

>>> matcher = soupmatchers.HTMLContains(
...    soupmatchers.Tag("testtools link", "a",
...        attrs={"href": "https://launchpad.net/testtools",
...               "class": "awesome"}))
>>> mismatch = matcher.match(
...    "<a href='https://launchpad.net/testtools'></a>")
>>> print(mismatch.describe())
Matched 0 times
Here is some information that may be useful:
   1 matches for "testtools link" when attribute class="awesome" is not a
   requirement.
>>> matcher = soupmatchers.HTMLContains(
...    soupmatchers.Tag("bold rocks", "b", text="rocks"))
>>> mismatch = matcher.match(
...    "<b>is awesome</b>")
>>> print(mismatch.describe())
Matched 0 times
Here is some information that may be useful:
  1 matches for "bold rocks" when text="rocks" is not a requirement.

尽管这通常不会告诉你多少有助于诊断问题的信息,但你应该能够以这种方式编写你的匹配器,以便输出通常是有用的。

将匹配限制在文档的特定区域

通常你希望断言一些 HTML 包含在文档的特定部分中。在最简单的情况下,你可能只想检查 HTML 是否在 <body> 标签内。

通过在 Within 匹配器中组合它们,可以指定某个 Tag 包含在另一个 Tag 中。

>>> child_matcher = soupmatchers.Tag("bold rocks", "b", text="rocks")
>>> body_matcher = soupmatchers.Tag("the body", "body")
>>> matcher = soupmatchers.HTMLContains(
...     soupmatchers.Within(body_matcher, child_matcher))
>>> print(matcher)
HTML contains [Tag("bold rocks", <b ...>rocks ...</b>)
within Tag("the body", <body ...>...</body>)]
>>> mismatch = matcher.match("<b>rocks</b><body></body>")
>>> print(mismatch.describe())
Matched 0 times
Here is some information that may be useful:
  1 matches for "bold rocks" in the document.
  1 matches for "the body" in the document.

项目详情


下载文件

下载适用于您平台文件。如果您不确定选择哪个,请了解更多关于 安装包 的信息。

源代码发行版

soupmatchers-0.5.tar.gz (19.5 kB 查看哈希值)

上传时间 源代码

构建发行版

soupmatchers-0.5-py3-none-any.whl (10.4 kB 查看哈希值)

上传时间 Python 3

支持