如何模拟 urllib.requests.urlopen?

Mav*_*eeh 5 python unit-testing mocking urllib pytest

我正在制作一个屏幕抓取器,用于从电视频道网站收集时间表数据。为此,我必须获取网站的原始 HTML。我是第一次尝试单元测试,我对模拟有一些了解,但我对此还不太有信心。(如果有人能提供一些关于这方面的好的学习资源,我将不胜感激。)

我的方法是尝试返回一个时间表类,该类将从 HTML 数据中解析 DOM 并剥离时间表数据,如下所示:

from urllib.request import urlopen

def fetch_html():
    url = "https://example.com"
    response = urlopen(url)
    dom = response.read()
    return SomeScheduleClass(dom)
Run Code Online (Sandbox Code Playgroud)

我的观点是我不知道如何测试它是否确实SomeScheduleClass按预期返回。我是否嘲笑http.client.HTTPResponse从 得到的对象urlopen?我是否能找到一种方法来嘲笑response.read()?或者我根本不需要测试这个,我只是在浪费时间?如果是这样,我应该在这里测试什么?

顺便说一句,我仍然很生疏,因为我所知道的关于Python的一切都是自学的,所以如果这是非常基础的并且我在浪费每个人的时间,我会非常乐意删除这个问题并忘记它曾经存在过。提前致谢!

Ous*_*uss -4

简洁版本

现在我们感兴趣的是控制 的预期结果fetch_html(),这意味着我们必须控制函数的输入。该函数用于urllib.request.urlopen()从域的http服务器获取响应example.com。该 url 实际上是函数的输入。为了控制它并确保我们可以运行测试而不必向“example.com”发出任何“真实”http 请求,我们必须模拟 ,urllib.request.urlopen()以便它返回预定义的response. 一种方法是修补 urllib.request.urlopen() 以返回一个类的实例,该类Response通过公开返回预加载 HTML 的 .read() 函数来进行模拟。

这是一个使用的实现unittest

import unittest
from unittest.mock import patch
from src.schedules import SomeScheduleClass
from src.utils import fetch_html

class TestFetchHtml(unittest.TestCase):
    # mocked html content. It can be re-used when testing SomeScheduleClass
    mock_html_content = b"<html>Mock example.com HTML content</html>"

    # class to mock the http response class returned by urlopen
    class MockResponse:
        def read(self):
            return TestFetchHtml.mock_html_content

    # Prepare mocking
    def setUp(self):
        self.mock_response = TestFetchHtml.MockResponse()

    def test_fetch_html(self):
        with patch('urllib.request.urlopen', return_value=self.mock_response) as mocked_urlopen:
            # call the function to be tested
            result = fetch_html()
            mocked_urlopen.assert_called_once_with("https://example.com")

            # assert the return value is an object of type SomeScheduleClass
            self.assertIsInstance(result, SomeScheduleClass)

            # some assertions to make sure result contains the expected value
            # it should be extensive since we are not testing SomeScheduleClass here
            # Additional assertions could be added here as needed
Run Code Online (Sandbox Code Playgroud)

这是一个使用的实现pytest

你必须首先安装:

pip3 install pytest pytest-mock
Run Code Online (Sandbox Code Playgroud)

pytest-mock提供的装置mocker是 的功能的薄包装unittest.mock

那么你可以在函数中使用pytest, 和 ,如下所示:mockertest_fetch_html()

import pytest
from src.schedules import SomeScheduleClass
from src.utils import fetch_html

@pytest.mark.UNIT
def test_fetch_html(mocker):
    # mocked html content. It can be re-used when testing SomeScheduleClass
    mock_html_content = b"<html>Mock example.com HTML content</html>"
    # class to mock the http response class returned by urlopen
    class MockResponse:
        def read(self):
            return mock_html_content
    mock_response = MockResponse()
    # mock urllib.request.urlopen to return mock_response
    p = mocker.patch('urllib.request.urlopen', return_value=mock_response)
    # call the function to be tested
    r = fetch_html()
    # assert urlopen was called once with url: https://example.com
    p.assert_called_once_with("https://example.com")
    # assert the return value is an object of type SomeScheduleClass
    assert isinstance(r, SomeScheduleClass)
    # some assertions to make sure r contains the exected value
    # it should be be extensive since we are not testing SomeScheduleClass
Run Code Online (Sandbox Code Playgroud)

长版

这是一个非常普遍的问题,值得一个普遍的答案。Althgouh Stackoverflow 是一个获得答案的好地方,我不相信这里的答案无论多好都不能替代更多实质性的阅读、课程和实践经验。尽管如此,我会尽力做到彻底。

测试自动化是软件开发中的热门话题,是SecDevOps的重要组成部分,在现代软件开发中发挥着非常重要的作用。我个人是在 30 多年的软件开发生涯中才了解测试自动化的,我内心的一部分希望自己能够更早地开始编写测试或测试驱动的软件开发,因此,我鼓励您继续沿着这条道路进行测试自动化技能更上一层楼。

我将假设什么是测试自动化?,以及为什么测试自动化很重要?您已经清楚了,我将继续讨论下一个问题要测试什么?以及何时进行测试?:

要测试什么?

在你的问题中你写道,我引用:

Blockquote 我的观点是我不知道如何测试它是否确实按预期返回 SomeScheduleClass 。我是否模拟从 urlopen 获取的 http.client.HTTPResponse 对象?我是否找到了一种方法来模拟response.read()?或者我根本不需要测试这个,我只是在浪费时间?如果是这样,我应该在这里测试什么?

这是一个重要的问题。为现有代码编写测试是“工作”并且“需要”时间和精力,因此“为什么”问题不仅仅是一个哲学问题,而且也是一个工程问题。质量保证经理、CI/CD 管道经理或 CIO 会实施或偏好多种方法和策略。我将尝试列出一些策略,我希望这些策略对解决一般要测试什么?的问题有用。问题,那么我将解决有关您共享的代码片段的问题。

使用测试驱动开发

测试驱动开发意味着在编写代码之前(或同时)编写测试。这种方法可以确保高测试覆盖率,并且还迫使您以易于测试的方式设计代码。

专注于回归测试

如果您没有实现测试驱动程序开发,并且您的代码覆盖率不高,那么您需要在每次发现代码中的错误时开始编写测试。编写这些回归测试的目的是确保错误在修复后不会再次出现。此外,编写回归测试会慢慢增加代码覆盖率,直到达到可以切换到测试驱动开发的程度。如果您已经在实施测试驱动开发,那么添加回归测试将涵盖您在之前的测试中未解决的情况,并确保错误不会在代码重构时再次出现。换句话说,错误修复意味着编写重现错误情况的测试代码,然后修复错误,从而避免错误并使测试通过。

专注于代码中最关键的部分:

编写测试非常耗时。如果您打算将这些时间投入到编写测试中,那么请投入时间来覆盖代码中最重要的部分,或者可能存在错误的最困难的部分。这将使您能够发现/避免运行时错误,并且还可以帮助您重新设计代码,使其更加模块化和简单。

为您的代码编写测试

不要浪费时间为第 3 方包的功能编写测试。大多数软件包已经进行了测试。为代码编写测试,测试函数的所有分支,测试类或模块中的所有函数,测试所有模块。使用测试覆盖率分析工具可以轻松放大测试未覆盖的代码。

专注于接口

在编写与第 3 方服务或 API 接口的代码时,关注这些接口并不是一个坏主意,因为它允许您检测 api 中的任何更改

设定覆盖目标:

并添加一条规则,例如确保代码提交时,覆盖率不会降低。

安全驱动的测试编写:

通过此策略,目标是确保涵盖所有边缘情况、压力情况或任何安全问题。在这里,您编写旨在破坏系统的测试,然后调整代码,例如系统不会破坏,或者系统以预期和受控的方式失败。每次发现安全问题时,解决该问题意味着编写一个重现该问题的测试并修复代码以使测试通过,这类似于编写回归测试。

关注用例:

编写涵盖代码用例的测试,或者由代码/工具的最终用户驱动的测试。这样你就可以涵盖与用户体验相关的部分。通过减少新功能或代码修复的开发/手动测试迭代,可以加快向最终用户交付功能和修复的速度。

代码片段中要测试的内容:

from urllib.request import urlopen

def fetch_html():
    url = "https://example.com"
    response = urlopen(url)
    dom = response.read()
    return SomeScheduleClass(dom)
Run Code Online (Sandbox Code Playgroud)

我将首先为 fetch_html 编写单元测试,该测试应确保该函数返回 SomeScheduleClass 类型的实例,并且该实例的内容是dom使用SomeScheduleClass. 我们对测试不感兴趣,urllib.request.urlopen因为这不是您的代码。我们对测试 SomeScheduleClass 构造函数不感兴趣,test_fetch_html()因为它应该在类的单元测试中进行测试SomeScheduleClass

现在我们感兴趣的是控制 的预期结果fetch_html(),这意味着我们必须控制函数的输入。该函数用于urllib.request.urlopen()从域的http服务器获取响应example.com。该 url 实际上是函数的输入。为了控制它并确保我们可以运行测试而不必向“example.com”发出任何“真实”http 请求,我们必须模拟 ,urllib.request.urlopen()以便它返回预定义的response. 一种方法是修补 urllib.request.urlopen() 以返回一个类的实例,该类Response通过公开返回预加载 HTML 的 .read() 函数来进行模拟。

这是一个使用的实现unittest

import unittest
from unittest.mock import patch
from src.schedules import SomeScheduleClass
from src.utils import fetch_html

class TestFetchHtml(unittest.TestCase):
    # mocked html content. It can be re-used when testing SomeScheduleClass
    mock_html_content = b"<html>Mock example.com HTML content</html>"

    # class to mock the http response class returned by urlopen
    class MockResponse:
        def read(self):
            return TestFetchHtml.mock_html_content

    # Prepare mocking
    def setUp(self):
        self.mock_response = TestFetchHtml.MockResponse()

    def test_fetch_html(self):
        with patch('urllib.request.urlopen', return_value=self.mock_response) as mocked_urlopen:
            # call the function to be tested
            result = fetch_html()
            mocked_urlopen.assert_called_once_with("https://example.com")

            # assert the return value is an object of type SomeScheduleClass
            self.assertIsInstance(result, SomeScheduleClass)

            # some assertions to make sure result contains the expected value
            # it should be extensive since we are not testing SomeScheduleClass here
            # Additional assertions could be added here as needed
Run Code Online (Sandbox Code Playgroud)

这是一个使用的实现pytest

你必须首先安装:

pip3 install pytest pytest-mock
Run Code Online (Sandbox Code Playgroud)

pytest-mock提供的装置mocker是 的功能的薄包装unittest.mock

那么你可以在函数中使用pytest, 和 ,如下所示:mockertest_fetch_html()

import pytest
from src.schedules import SomeScheduleClass
from src.utils import fetch_html

@pytest.mark.UNIT
def test_fetch_html(mocker):
    # mocked html content. It can be re-used when testing SomeScheduleClass
    mock_html_content = b"<html>Mock example.com HTML content</html>"
    # class to mock the http response class returned by urlopen
    class MockResponse:
        def read(self):
            return mock_html_content
    mock_response = MockResponse()
    # mock urllib.request.urlopen to return mock_response
    p = mocker.patch('urllib.request.urlopen', return_value=mock_response)
    # call the function to be tested
    r = fetch_html()
    # assert urlopen was called once with url: https://example.com
    p.assert_called_once_with("https://example.com")
    # assert the return value is an object of type SomeScheduleClass
    assert isinstance(r, SomeScheduleClass)
    # some assertions to make sure r contains the exected value
    # it should be be extensive since we are not testing SomeScheduleClass
Run Code Online (Sandbox Code Playgroud)

很明显,该函数没有任何错误处理,并且引发的任何异常都将由调用函数处理。因此,在测试调用函数时,应解决边缘情况,例如无法访问 example.com 的情况,或者 example.com 返回 4xx 或 5xx 或类似的情况。

什么时候测试?

即何时运行测试,在开发期间,在提交到分支之后?关于合并请求?上演?部署时?在“The DevOps Handbook”(我强烈推荐)中,在讨论“第二种方式,反馈原则”时,建议是“不断推动质量更接近源头”和“为下游团队提供优化”。实际上,开发人员应该能够在提交代码之前对其进行测试(使质量更接近源代码),并且代码中的任何错误都应该在投入生产之前尽快发现,错误的发现越接近其源头, 更好。我建议让开发人员在提交代码之前运行所有测试。但这意味着开发人员应该能够自己创建类似生产的环境......这里有很多东西可以讨论。

贸易工具:

单元测试

Python 附带了使用 unittest 进行测试的内置工具https://docs.python.org/3/library/unittest.html

py测试

我个人更喜欢 pytest https://pytest.org/框架,原因有很多,例如:

  • pytest 使组织测试比unittest 更简单、更灵活。
  • 它还使参数化测试变得更加简单。
  • 就代码行数而言也更经济
  • 如果您已经为unittest编写了测试,pytest仍然可以运行它们。

毒物

来自 tox 网站https://tox.wiki/en/4.13.0/:tox 是一个通用的虚拟环境管理和测试命令行工具,您可以使用

  • 检查您的包在不同环境(例如不同的 Python 实现、版本或安装依赖项)下是否正确构建和安装,
  • 使用所选的测试工具在每个环境中运行测试,
  • 作为持续集成服务器的前端,大大减少了样板文件并合并了 CI 和基于 shell 的测试。
  • 它是可扩展的,并且已经拥有大量用于模拟或其他测试相关功能的插件

资源:

我实际上正在编写一本使用 pytest 的书/udemy 课程,但它还没有准备好,在那之前我会推荐以下资源:

网站

培训班:

播客:

图书:

  • 不过,OP 没有使用“requests”,所以我不知道“requests-mock”会如何帮助他们。 (8认同)