Как имитировать 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
прямо сейчас, код зависит онлайн сервера. Не должно. Я хочу иметь возможность добавлять множество различных типов комбинаций таблиц стилей и тестировать протокол, а затем позже некоторые параметры их синтаксического анализа, комбинаций и т. д.
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 очень полезно для двух вещей:
- это позволяет избежать сюрпризов, когда ваш код тайно полагается на какой-то объект, чтобы существовать где-то
- оно позволяет тесту быть написанным который впрыскивает различное сортировка объектов в зависимости от цели этого теста
здесь, мы можем использовать макет объекта, который мы дадим 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-сервером на уровне модульных тестов, важно написать интеграционные тесты, которые позволят нашему приложению общаться с такими серверами, с которыми оно будет иметь дело в реальной жизни. Мы могли бы сделать это с полноценным сервером, настроенным специально для интеграционного тестирования, или написать надуманный.