Подтвердить что ты не робот

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

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

  • Запустите сервер
  • Создайте несколько ресурсов (URI) с соответствующими типами mime, кодом ответа и т.д.
  • Запустите тесты (было бы неплохо не запускать сервер для каждого теста)
  • Выключите сервер.

Любые подсказки для этого кода будут полезны. Я пробовал несколько вещей с 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

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

4b9b3361

Ответ 1

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

Если вы хотите написать на самом деле модульные тесты, тогда вы должны создать свои собственные тестовые входы, а также посмотреть штучные объекты. Python - динамический язык, насмешливый и обезьянный путь - легкие и мощные инструменты для записи unit test. В частности, посмотрите на отличный модуль Mock.

Простой unit test

Итак, если мы посмотрим на ваш пример CssTests, вы пытаетесь проверить, что css.getCssUriList может извлечь всю таблицу стилей CSS, на которую ссылается часть HTML, которую вы ей даете. Что вы делаете в этом конкретном unit test, не тестируете, что вы можете отправить запрос и получить ответ с веб-сайта, верно? Вы просто хотите удостовериться, что, учитывая некоторый 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: это называется Injection Dependency (DI).

DI очень полезен для двух вещей:

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

Здесь мы можем использовать макет-объект, который мы дадим 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 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 и протестировали их, и это касается того, насколько мы должны идти на уровне unit test. Запрос теперь находится в руке этой библиотеки, и, конечно же, это не наша программа unit test, чтобы проверить, работает ли библиотека как ожидалось.

Патч обезьяны

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

Наш unit test будет выглядеть примерно так:

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 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

Заключение

Очень важно держать unit test маленьким, простым, быстрым и автономным. A unit test, который полагается на другой сервер для запуска, просто не является unit test. Чтобы помочь в этом, DI - отличная практика, и имитирует объекты отличный инструмент.

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

P.S.

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