【问题标题】:Test Driven Development (TDD) for Web ScrapingWeb Scraping 的测试驱动开发 (TDD)
【发布时间】:2017-05-10 12:39:50
【问题描述】:

总结

我有一个Python based web scraping pet project,我正在尝试在其中实现一些 TDD,但我很快遇到了问题。单元测试需要互联网连接,以及下载 html 文本。虽然我知道实际的 解析 可以使用本地文件完成,但有些方法用于简单地重新定义 URL 并再次查询网站。这似乎打破了 TDD 的一些最佳实践(引用:Robert Martin 的 Clean Code 声称测试应该在任何环境中都可以运行)。虽然这是一个 Python 项目,但我在使用 R 进行 Yahoo Finance 抓取时遇到了类似的问题,我确信这种事情与语言无关。至少,这个问题似乎违反了 TDD 中的一个主要准则,即测试应该快速运行。

tldr;是否有在 TDD 中处理网络连接的最佳实践?

可重现的示例

AbstractScraper.py

from urllib.request import urlopen
from bs4 import BeautifulSoup


class AbstractScraper:

    def __init__(self, url):
        self.url = url
        self.dataDictionary = None

    def makeDataDictionary(self):
        html = urlopen(self.url)
        text = html.read().decode("utf-8")
        soup = BeautifulSoup(text, "lxml")
        self.dataDictionary = {"html": html, "text": text, "soup": soup}

    def writeSoup(self, path):
        with open(path, "w") as outfile:
            outfile.write(self.dataDictionary["soup"].prettify())

TestAbstractScraper.py

import unittest
from http.client import HTTPResponse
from bs4 import BeautifulSoup
from CrackedScrapeProject.scrape.AbstractScraper import AbstractScraper
from io import StringIO


class TestAbstractScraperMethods(unittest.TestCase):

    def setUp(self):
        self.scraper = AbstractScraper("https://docs.python.org/2/library/unittest.html")
        self.scraper.makeDataDictionary()

    def test_dataDictionaryContents(self):
        self.assertTrue(isinstance(self.scraper.dataDictionary, dict))
        self.assertTrue(isinstance(self.scraper.dataDictionary["html"], HTTPResponse))
        self.assertTrue(isinstance(self.scraper.dataDictionary["text"], str))
        self.assertTrue(isinstance(self.scraper.dataDictionary["soup"], BeautifulSoup))
        self.assertSetEqual(set(self.scraper.dataDictionary.keys()), set(["text", "soup", "html"]))

    def test_writeSoup(self):
        filePath = "C:/users/athompson/desktop/testFile.html"
        self.scraper.writeSoup(filePath)
        self.writtenData = open(filePath, "r").read()
        self.assertEqual(self.writtenData, self.scraper.dataDictionary["soup"].prettify())

if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(TestAbstractScraperMethods)
    unittest.TextTestRunner(verbosity=2).run(suite)

【问题讨论】:

  • 我建议模拟网络连接;那么您不仅不需要互联网连接,而且您可以绝对控制模拟连接返回的内容(然后您不会因网络故障和/或雅虎/等更改页面而导致虚假测试失败)。 docs.python.org/3/library/unittest.mock.html :) (当然,如果您尝试测试 yahoo/etc 没有更改页面,这将无济于事。)
  • 单元测试从不需要任何连接。必须模拟测试单元之外的所有内容。测试连接可能在行为或集成测试中完成。
  • 考虑从 bs 切换到 scrapy,它是一个强大的抓取工具,也可以自动化很多事情。此外,通过大量模块轻松学习。

标签: python unit-testing testing web-scraping


【解决方案1】:

正如您所说,在 TDD 期间运行的测试必须快速运行,并且还有其他方面,例如确定性等(那么,如果连接中断怎么办?)。正如 cmets 中提到的,这通常意味着您必须对那些令人不安的依赖项使用模拟。

然而,这里有一个基本假设:即,您正在编写的代码可以通过单元测试进行明智的测试。这是什么意思?这意味着单元测试很有可能会发现错误。换句话说,如果极不可能通过单元测试找到错误,那么单元测试就不是正确的做法。

关于你的函数makeDataDictionary,它主要由对依赖项的调用组成。因此,集成测试(即检查代码如何与其使用的真实库交互的测试)似乎可能有助于发现错误:您的代码是否使用正确的参数正确调用库?该库是否以您期望的方式实际提供结果?交互顺序是否正确?模拟库不会回答这些问题:如果您对使用的库的假设是错误的,那么您将根据错误的假设来实现模拟。

另一方面,如果你从 makeDataDictionary 中模拟掉所有依赖项,你希望找到什么错误?可能(在函数的最后一行)数据字典本身的创建可能是错误的(例如,键的名称错误)。因此,在我看来,这一行是makeDataDictionary 中唯一有意义的实际单元测试部分。

因此,在这种情况下,我的建议是首先将纯逻辑(算法代码)的代码与交互主导的代码分开。例如,创建一个辅助方法_makeDataDictionary(html, text, soup),它只返回{"html": html, "text": text, "soup": soup}。然后,对_makeDataDictionary 应用单元测试,而不是makeDataDictionary。相比之下,使用集成测试来测试 makeDataDictionary

这也节省了大量的模拟工作:对于单元测试_makeDataDictionary,不需要模拟。对于集成测试makeDataDictionary,模拟毫无意义。对于调用 makeDataDictionary 并进行单元测试的代码,最好将调用 makeDataDictionary 作为一个整体而不是替换其各个依赖项。

然而,在 TDD 上下文中,这有点难以处理:TDD 似乎没有不适合单元测试的代码概念。但是,通过适当的提前思考(也称为设计阶段),您可以及早识别是否应该将算法代码与交互主导代码分开。另一个不应误导人们相信 TDD 消除了对某些适当设计工作的需要的示例。

【讨论】:

    【解决方案2】:

    模拟 http 请求不是一项简单的任务,您可能需要 html 之外的更多信息,但是有一些包可以让您记录所有数据的 http 请求 我建议您查看 betamaxvcr

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-02-11
      • 1970-01-01
      • 2014-01-30
      • 1970-01-01
      • 1970-01-01
      • 2011-06-07
      • 1970-01-01
      相关资源
      最近更新 更多