【问题标题】:How to Mock an HTTP request in a unit testing scenario in Python如何在 Python 的单元测试场景中模拟 HTTP 请求
【发布时间】:2012-07-09 02:14:23
【问题描述】:

我想为所有与 HTTP 相关的测试包括一个 Web 服务器。它不需要非常复杂。我宁愿不依赖于在线。所以我可以测试我的程序的一些选项。

  1. 启动服务器
  2. 使用适当的 mime 类型、响应代码等创建一些资源 (URI)。
  3. 运行测试(最好不必为每个测试启动服务器)
  4. 关闭服务器。

有关此代码的任何提示都会有所帮助。我用 BaseHTTPServer 尝试了一些东西,但还没有成功。 nosetests 命令似乎无限期地等待。

import unittest
from foo import core

class HttpRequests(unittest.TestCase):
    """Tests for HTTP"""

    def setUp(self):
        "Starting a Web server"
        self.port = 8080
        # Here we need to start the server
        #
        # Then define a couple of URIs and their HTTP headers
        # so we can test the code.
        pass

    def testRequestStyle(self):
        "Check if we receive a text/css content-type"
        myreq = core.httpCheck()
        myuri = 'http://127.0.0.1/style/foo'
        myua = "Foobar/1.1"
        self.asserEqual(myreq.mimetype(myuri, myua), "text/css")

    def testRequestLocation(self):
        "another test" 
        pass

    def tearDown(self):
        "Shutting down the Web server"
        # here we need to shut down the server
        pass

感谢您的帮助。


更新 - 2012:07:10T02:34:00Z

这是一个代码,对于给定的网站将返回 CSS 列表。我想测试它是否返回正确的 CSS 列表。

import unittest
from foo import core

class CssTests(unittest.TestCase):
    """Tests for CSS requests"""

    def setUp(self):
        self.css = core.Css()
        self.req = core.HttpRequests()

    def testCssList(self):
        "For a given Web site, check if we get the right list of linked stylesheets"
        WebSiteUri = 'http://www.opera.com/'
        cssUriList = [
        'http://www.opera.com/css/handheld.css',
        'http://www.opera.com/css/screen.css',
        'http://www.opera.com/css/print.css',
        'http://www.opera.com/css/pages/home.css']
        content = self.req.getContent(WebSiteUri)
        cssUriListReq = self.css.getCssUriList(content, WebSiteUri)
        # we need to compare ordered list.
        cssUriListReq.sort()
        cssUriList.sort()
        self.assertListEqual(cssUriListReq, cssUriList)

然后在foo/core.py

import urlparse
import requests
from lxml import etree
import cssutils

class Css:
    """Grabing All CSS for one given URI"""


    def getCssUriList(self, htmltext, uri):
        """Given an htmltext, get the list of linked CSS"""
        tree = etree.HTML(htmltext)
        sheets = tree.xpath('//link[@rel="stylesheet"]/@href')
        for i, sheet in enumerate(sheets):
            cssurl = urlparse.urljoin(uri, sheet)
            sheets[i] = cssurl
        return sheets

目前,代码依赖于在线服务器。它不应该。我希望能够添加大量不同类型的样式表组合并测试协议,然后在它们的解析、组合等方面进行一些选项。

【问题讨论】:

  • 我完全同意@Rodrigue 的观点,即为单元测试启动 Web 服务器绝对不是一个好习惯。但是如果你想进行一些功能测试,你可以考虑使用代理WireMock等现成的解决方案。它具有 HTTP API 来配置和获取已通过它传递的请求。

标签: python unit-testing http mocking monkeypatching


【解决方案1】:

为单元测试启动 Web 服务器绝对不是一个好习惯。单元测试应该简单且隔离,这意味着它们应该避免执行例如 IO 操作。

如果你想写的是真正的单元测试,那么你应该制作自己的测试输入,并查看mock objects。 Python 作为一种动态语言,模拟和猴子路径是编写单元测试的简单而强大的工具。特别是看看优秀的Mock module

简单的单元测试

因此,如果我们查看您的 CssTests 示例,您正在尝试测试 css.getCssUriList 是否能够提取您提供的一段 HTML 中引用的所有 CSS 样式表。您在这个特定的单元测试中所做的并不是测试您可以发送请求并从网站获得响应,对吗?您只是想确保给定一些 HTML,您的函数返回正确的 CSS URL 列表。因此,在这个测试中,您显然不需要与真正的 HTTP 服务器通信。

我会做如下的事情:

import unittest

class CssListTestCase(unittest.TestCase):

    def setUp(self):
        self.css = core.Css()

    def test_css_list_should_return_css_url_list_from_html(self):
        # Setup your test
        sample_html = """
        <html>
            <head>
                <title>Some web page</title>
                <link rel='stylesheet' type='text/css' media='screen'
                      href='http://example.com/styles/full_url_style.css' />
                <link rel='stylesheet' type='text/css' media='screen'
                      href='/styles/relative_url_style.css' />
            </head>
            <body><div>This is a div</div></body>
        </html>
        """
        base_url = "http://example.com/"

        # Exercise your System Under Test (SUT)
        css_urls = self.css.get_css_uri_list(sample_html, base_url)

        # Verify the output
        expected_urls = [
            "http://example.com/styles/full_url_style.css",
            "http://example.com/styles/relative_url_style.css"
        ]
        self.assertListEqual(expected_urls, css_urls)    

依赖注入模拟

现在,不太明显的事情是对core.HttpRequests 类的getContent() 方法进行单元测试。我想您正在使用 HTTP 库,而不是在 TCP 套接字上发出自己的请求。

为了使您的测试保持在 unit 级别,您不希望通过网络发送任何内容。您可以做些什么来避免这种情况,即进行测试以确保您正确使用 HTTP 库。这不是测试代码的行为,而是测试它与周围其他对象的交互方式。

这样做的一种方法是明确对该库的依赖:我们可以向HttpRequests.__init__ 添加一个参数,以将一个库的HTTP 客户端实例传递给它。假设我使用了一个 HTTP 库,它提供了一个 HttpClient 对象,我们可以在该对象上调用 get()。你可以这样做:

class HttpRequests(object):

    def __init__(self, http_client):
        self.http_client = http_client

   def get_content(self, url):
        # You could imagine doing more complicated stuff here, like checking the
        # response code, or wrapping your library exceptions or whatever
        return self.http_client.get(url)

我们已经明确了依赖关系,现在需要HttpRequests 的调用者来满足要求:这称为依赖注入 (DI)。

DI 在两件事上非常有用:

  1. 它避免了您的代码秘密依赖某个对象存在于某处的意外情况
  2. 它允许编写测试,根据测试的目标注入不同类型的对象

在这里,我们可以使用我们将提供给core.HttpRequests 的模拟对象,并且它会在不知不觉中使用它,就好像它是真正的库一样。之后,我们可以测试交互是否按预期进行。

import core

class HttpRequestsTestCase(unittest.TestCase):

    def test_get_content_should_use_get_properly(self):
        # Setup

        url = "http://example.com"

        # We create an object that is not a real HttpClient but that will have
        # the same interface (see the `spec` argument). This mock object will
        # also have some nice methods and attributes to help us test how it was used.
        mock_http_client = Mock(spec=somehttplib.HttpClient) 

        # Exercise

        http_requests = core.HttpRequests(mock_http_client)
        content = http_requests.get_content(url)

        # Here, the `http_client` attribute of `http_requests` is the mock object we
        # have passed it, so the method that is called is `mock.get()`, and the call
        # stops in the mock framework, without a real HTTP request being sent.

        # Verify

        # We expect our get_content method to have called our http library.
        # Let's check!
        mock_http_client.get.assert_called_with(url)

        # We can find out what our mock object has returned when get() was
        # called on it
        expected_content = mock_http_client.get.return_value
        # Since our get_content returns the same result without modification,
        # we should have received it
        self.assertEqual(content, expected_content)

我们现在已经测试了我们的 get_content 方法与我们的 HTTP 库正确交互。我们已经定义了HttpRequests 对象的边界并对其进行了测试,这就是我们应该在单元测试级别进行的程度。该请求现在已掌握在该库的手中,我们的单元测试套件当然不会负责测试该库是否按预期工作。

猴子补丁

现在假设我们决定使用伟大的requests library。它的 API 更加程序化,它没有提供我们可以抓取来发出 HTTP 请求的对象。相反,我们将导入模块并调用其get 方法。

core.py 中的 HttpRequests 类将如下所示:

import requests

class HttpRequests(object):

    # No more DI in __init__

    def get_content(self, url):
        # We simply delegate the HTTP work to the `requests` module
        return requests.get(url)

没有更多的 DI,所以现在,我们想知道:

  • 如何防止发生网络交互?
  • 如何测试我是否正确使用了requests 模块?

您可以在这里使用动态语言提供的另一种奇妙但有争议的机制:monkey patching。我们将在运行时将requests 模块替换为我们制作并可以在测试中使用的对象。

我们的单元测试将如下所示:

import core

class HttpRequestsTestCase(unittest.TestCase):

    def setUp(self):
        # We create a mock to replace the `requests` module
        self.mock_requests = Mock()

        # We keep a reference to the current, real, module
        self.old_requests = core.requests

        # We replace the module with our mock
        core.requests = self.mock_requests

    def tearDown(self):
        # It is very important that each unit test be isolated, so we need
        # to be good citizen and clean up after ourselves. This means that
        # we need to put back the correct `requests` module where it was
        core.requests = self.old_requests

    def test_get_content_should_use_get_properly(self):
        # Setup

        url = "http://example.com"

        # Exercise
        http_client = core.HttpRequests()
        content = http_client.get_content(url)

        # Verify

        # We expect our get_content method to have called our http library.
        # Let's check!
        self.mock_requests.get.assert_called_with(url)

        # We can find out what our mock object has returned when get() was
        # called on it
        expected_content = self.mock_requests.get.return_value
        # Since our get_content returns the same result without modification,
        # we should have received
        self.assertEqual(content, expected_content)

为了使这个过程不那么冗长,mock 模块有一个 patch 装饰器来处理脚手架。然后我们只需要写:

import core

class HttpRequestsTestCase(unittest.TestCase):

    @patch("core.requests")
    def test_get_content_should_use_get_properly(self, mock_requests):
        # Notice the extra param in the test. This is the instance of `Mock` that the
        # decorator has substituted for us and it is populated automatically.

        ...

        # The param is now the object we need to make our assertions against
        expected_content = mock_requests.get.return_value

结论

保持单元测试的规模小、简单、快速和独立是非常重要的。依赖另一台服务器运行的单元测试根本不是单元测试。为此,DI 是一种很好的实践,而模拟对象则是一种很好的工具。

首先,要理解模拟的概念以及如何使用它们并不容易。像每个电动工具一样,它们也可能在您的手中爆炸,例如让您相信您已经测试过某些东西,而实际上您并没有。确保模拟对象的行为和输入/输出反映现实至关重要。

附言

鉴于我们从未在单元测试级别与真正的 HTTP 服务器进行过交互,因此编写集成测试以确保我们的应用程序能够与它将在现实生活中处理的那种服务器进行通信非常重要。我们可以使用专门为集成测试设置的成熟服务器来做到这一点,或者编写一个人为的服务器。

【讨论】:

  • 我还没有core.httpCheck的代码,因为我正在为它做测试。
  • 好的。不过我很困惑:httpCheck 创建的对象是您要测试的东西吗?
  • 罗德里格,这很有趣。我还没有完全理解。我想我将不得不尝试它来测试它。为了给你一个更具体的例子,它涉及到我正在尝试做的事情,我将在上面编辑我今天下午所做的事情。
  • 我已编辑。我希望它澄清。感谢您到现在为止的帮助。不确定它是否回答了我最初的问题。
  • 我根据您更新的代码示例更新了我的答案。我希望它有意义并且可以帮助你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-26
  • 2014-08-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多