34

我想为所有与 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

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

4

1 回答 1

75

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

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

简单的单元测试

因此,如果我们查看您的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 客户端实例。假设我使用了一个 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 库。它的 API 更加程序化,它没有提供我们可以从中获取 HTTP 请求的对象。相反,我们将导入模块并调用它的get方法。

我们的HttpRequestscore.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是否正确使用了该模块?

在这里,您可以使用动态语言提供的另一种奇妙但有争议的机制:猴子补丁。我们将在运行时将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 是一个很好的实践,而模拟对象是一个很好的工具。

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

附言

Given that we have never interacted with a real HTTP server at the unit test level, it is important to write Integration Tests that will make sure our application is able to talk to the sort of servers it will deal with in real life. We could do this with a fully fledged server set up specially for Integration Testing, or write a contrived one.

于 2012-07-09T16:25:18.550 回答