Jak wyśledzić żądanie HTTP w scenariuszu testów jednostkowych w Pythonie

Chciałbym dołączyć serwer WWW dla wszystkich moich testów związanych z HTTP. Nie musi być bardzo wyrafinowany. Wolałbym nie być uzależniony od bycia online. Więc mogę przetestować kilka opcji mojego programu.

  1. Uruchom serwer
  2. utwórz kilka zasobów (URI) z odpowiednimi typami mime, kodem odpowiedzi itp.
  3. Uruchom testy (dobrze byłoby nie uruchamiać serwera dla każdego testu)
  4. Wyłącz serwer.

Wszelkie wskazówki na ten temat kod byłby pomocny. Próbowałem kilku rzeczy z BaseHTTPServer, ale nie udało się jeszcze. polecenie nosetests wydaje się czekać w nieskończoność.

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
Dzięki za pomoc.

Aktualizacja-2012:07:10T02:34: 00Z

Jest to kod, który dla danej strony internetowej zwróci listę CSS. Chcę sprawdzić, czy zwraca właściwą listę 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)

Następnie w 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
W tej chwili kod zależy od serwera online. Nie powinno. Chcę móc aby dodać wiele różnych typów kombinacji arkuszy stylów i przetestować protokół, a następnie niektóre opcje dotyczące ich parsowania, kombinacji itp.
Author: Rodrigue, 2012-07-09

1 answers

Uruchamianie serwera www do testów jednostkowych zdecydowanie nie jest dobrą praktyką. Testy jednostkowe powinny być proste i izolowane, co oznacza, że powinny unikać wykonywania operacji IO na przykład.

Jeśli to, co chcesz napisać, to naprawdę testy jednostkowe, powinieneś stworzyć własne wejścia testowe, a także zajrzeć do mock objects. Python jako dynamiczny język, szyderczy i Małpi pathing są łatwymi i potężnymi narzędziami do pisania testów jednostkowych. W szczególności zajrzyj na doskonały Mock module .

Prosty test jednostkowy

Więc, jeśli przyjrzymy się twojemu przykładowi CssTests, próbujesz sprawdzić, czy css.getCssUriList jest w stanie wyodrębnić cały arkusz stylów CSS, do którego odwołujesz się w podanym przez Ciebie fragmencie HTML. To, co robisz w tym konkretnym teście jednostkowym, nie jest testem, że możesz wysłać zapytanie i uzyskać odpowiedź ze strony internetowej, prawda? Po prostu chcesz się upewnić, że biorąc pod uwagę kod HTML, twoja funkcja zwraca poprawną listę adresów URL CSS. Tak więc w tym teście, oczywiście nie musisz rozmawiać z prawdziwym serwerem HTTP.

Zrobiłbym coś takiego:

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)    

Mocking with Dependency Injection

Teraz, czymś mniej oczywistym byłoby jednostkowe testowanie metody getContent() twojej klasy core.HttpRequests. Przypuszczam, że używasz biblioteki HTTP i nie składasz własnych żądań na gniazdach TCP.

Aby utrzymać testy na poziomie jednostki , nie chcesz niczego wysyłać przez kabel. Co można zrobić, aby uniknąć to jest posiadanie testów, które zapewniają, że poprawnie korzystasz z biblioteki HTTP. Chodzi tu nie o testowanie zachowania kodu, ale raczej jego interakcji z innymi obiektami wokół niego.

Możemy dodać parametr do HttpRequests.__init__, aby przekazać mu instancję klienta HTTP biblioteki. Powiedzmy, że używam biblioteki HTTP, która dostarcza HttpClient obiekt, na którym możemy wywołać get(). Mógłbyś coś zrobić. like:
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)

Uczyniliśmy zależność jawną i wymóg musi teraz zostać spełniony przez wywołującego HttpRequests: nazywa się to iniekcją zależności (DI).

DI jest bardzo przydatne do dwóch rzeczy:

  1. unika niespodzianek, gdy twój kod potajemnie opiera się na jakimś obiekcie, który gdzieś istnieje
  2. Pozwala na pisanie testów, które wprowadzają różnego rodzaju obiekty w zależności od celu tego testu.]}

Tutaj możemy użyć makiety obiektu, który damy core.HttpRequests i że będzie ona używać, nieświadomie, jak gdyby była prawdziwą biblioteką. Następnie możemy sprawdzić, czy interakcja została przeprowadzona zgodnie z oczekiwaniami.

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)

Przetestowaliśmy, czy nasza metoda get_content poprawnie współdziała z naszą biblioteką HTTP. Zdefiniowaliśmy granice naszego obiektu HttpRequests i przetestowaliśmy je, a to jest tak daleko, jak powinniśmy pójść na poziomie testu jednostkowego. Prośba jest teraz w rękach tej biblioteki i z pewnością nie jest to rola naszego zestawu testów jednostkowych aby sprawdzić, czy biblioteka działa zgodnie z oczekiwaniami.

Monkey patching

Teraz wyobraź sobie, że zdecydowaliśmy się użyć Wielkiej biblioteki zapytań . Jego API jest bardziej proceduralne, nie prezentuje obiektu, z którego możemy pobrać żądania HTTP. Zamiast tego zaimportowalibyśmy moduł i wywołalibyśmy jego metodę get.

Nasza HttpRequests klasa w core.py wyglądałaby następująco:

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)

Koniec z DI, więc teraz pozostaje nam zastanawiać się:

  • Jak czy mam zapobiegać interakcjom w sieci?
  • Jak mam sprawdzić czy poprawnie używam modułu requests?

Tutaj możesz użyć innego fantastycznego, ale kontrowersyjnego mechanizmu, który oferuje języki dynamiczne: monkey patching . Zamienimy, w czasie wykonywania, moduł requests na obiekt, który tworzymy i możemy użyć w naszym teście.

Nasz test jednostkowy będzie wyglądał następująco:

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)

Aby ten proces był mniej gadatliwy, moduł mock posiada dekoratora, który zajmuje się rusztowaniem. Następnie wystarczy napisać:

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

Podsumowanie

Bardzo ważne jest, aby testy jednostkowe były małe, proste, szybkie i samodzielne. Test jednostkowy, który polega na uruchomieniu innego serwera, nie jest po prostu testem jednostkowym. Aby w tym pomóc, DI jest świetną praktyką, a mock obiektów świetnym narzędziem.

Na początku nie jest łatwo zrozumieć koncepcję mocka i jak z nich korzystać. Jak każde elektronarzędzie, mogą również eksplodować w twoich rękach i na przykład sprawić, że uwierzysz, że coś przetestowałeś, podczas gdy w rzeczywistości tego nie zrobiłeś. Najważniejsze jest upewnienie się, że zachowanie i Wejście/Wyjście obiektów wzorcowych odzwierciedla rzeczywistość.

P. S.

Biorąc pod uwagę, że nigdy nie wchodziliśmy w interakcję z prawdziwym serwerem HTTP na poziomie testów jednostkowych, ważne jest, aby pisać testy integracyjne, które upewnią naszą aplikację, że będzie w stanie rozmawiać z takimi serwerami, z którymi będzie miała do czynienia w prawdziwym życiu. Moglibyśmy zrób to z pełnoprawnym serwerem skonfigurowanym specjalnie do testowania integracji lub napisz wymyślony.

 67
Author: Rodrigue,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-11 15:45:29