如何在Python中的单元测试场景中模拟HTTP请求

时间:2012-07-09 16:20:50

标签: python unit-testing http mocking monkeypatching

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

  1. 启动服务器
  2. 使用适当的mime类型,响应代码等创建一些资源(URI)
  3. 运行测试(最好不要为每个测试启动服务器)
  4. 关闭服务器。
  5. 对此代码的任何提示都会有所帮助。我用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
    

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

1 个答案:

答案 0 :(得分:71)

启动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)    

使用依赖注入进行模拟

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

要将测试保持在单元级别,您不希望通过网络发送任何内容。您可以采取哪些措施来避免这种情况,即可通过测试确保您正确使用HTTP库。这不是测试代码的行为,而是测试它与周围其他对象的交互方式。

这样做的一种方法是明确依赖于该库:我们可以向HttpRequests.__init__添加一个参数,以便将其传递给库的HTTP客户端实例。假设我使用提供HttpClient对象的HTTP库,我们可以在其上调用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. 它允许编写测试,根据测试的目标注入不同类型的对象
  3. 在这里,我们可以使用我们将提供给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方法。

    我们HttpRequests中的core.py课程会看起来如下:

    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是一个很好的实践,模拟对象是一个很好的工具。

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

    P.S。

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