Как имитировать HTTP-запрос в сценарии модульного тестирования в Python

Я хотел бы включить веб-сервер для всех моих тестов, связанных с HTTP. Он не должен быть очень сложным. Я бы предпочел не зависеть от интернета. Чтобы я мог протестировать некоторые варианты моей программы.

  1. запустить сервер
  2. Создайте несколько ресурсов (URI)с соответствующими типами mime, кодом ответа и т. д.
  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

прямо сейчас, код зависит онлайн сервера. Не должно. Я хочу иметь возможность добавлять множество различных типов комбинаций таблиц стилей и тестировать протокол, а затем позже некоторые параметры их синтаксического анализа, комбинаций и т. д.

1 ответов


запуск веб-сервера для модульного тестирования определенно не является хорошей практикой. Модульные тесты должны быть простыми и изолированными, что означает, что они должны избегать выполнения операций ввода-вывода, например.

если то, что вы хотите написать, действительно модульные тесты, то вы должны создать свои собственные тестовые входы, а также посмотреть в mock объектов. Python является динамическим языком, издевательство и обезьяна паттинг являются легкими и мощными инструментами для написания модульного теста. В частности, посмотрите в отличном макет модуля.

простой модульный тест

Итак, если мы посмотрим на свой CssTests пример, вы пытаетесь проверить, что css.getCssUriList способен извлечь все таблицы стилей CSS, на которые ссылаются в фрагменте HTML, который вы ему даете. То, что вы делаете в этом конкретном модульном тесте, не тестирует, что вы можете отправить запрос и получить ответ с веб-сайта, верно? Вы просто хотите убедиться, что с учетом некоторого HTML ваша функция возвращает правильный список URL-адреса в CSS. Таким образом, в этом тесте вам явно не нужно разговаривать с реальным 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 объект и протестировал их, и это так далеко, как мы должен идти на уровне модульного теста. Запрос теперь находится в руках этой библиотеки, и, конечно, не роль нашего набора модульных тестов проверить, что библиотека работает так, как ожидалось.

обезьяна ямочный

теперь представьте, что мы решили использовать запросы библиотека. Его 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 модуль, правильно?

здесь вы можете использовать еще один фантастический, но спорный механизм, который предлагают динамические языки:обезьяна латание. Мы заменим, во время выполнения 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)

чтобы сделать этот процесс менее verbose, то 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-отличная практика, а mock objects-отличный инструмент.

во-первых, это не легко получить вашу голову вокруг концепции макета и как их использовать, хотя. Как и любой электроинструмент, они также могут взорваться в ваших руках и, например, заставить вас поверить, что вы что-то проверили, в то время как на самом деле вы этого не сделали. Убедитесь, что поведение и ввод/вывод макетных объектов отражают реальность превыше всего.

П. С.

учитывая, что мы никогда не взаимодействовали с реальным HTTP-сервером на уровне модульных тестов, важно написать интеграционные тесты, которые позволят нашему приложению общаться с такими серверами, с которыми оно будет иметь дело в реальной жизни. Мы могли бы сделать это с полноценным сервером, настроенным специально для интеграционного тестирования, или написать надуманный.