Ответ 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, или написать надуманный.