web-dev-qa-db-fra.com

Comment se moquer d'une demande HTTP dans un scénario de test unitaire dans Python

Je voudrais inclure un serveur Web pour tous mes tests liés à HTTP. Il n'a pas besoin d'être très sophistiqué. Je préférerais ne pas être dépendant d'être en ligne. J'ai donc pu tester quelques options de mon programme.

  1. Démarrez le serveur
  2. Créez quelques ressources (URI) avec les types MIME appropriés, le code de réponse, etc.
  3. Exécutez les tests (ce serait bien de ne pas avoir à démarrer le serveur pour chaque test aussi)
  4. Arrêtez le serveur.

Toute indication sur ce code serait utile. J'ai essayé quelques choses avec BaseHTTPServer mais je n'ai pas encore réussi. La commande nosetests semble attendre indéfiniment.

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

merci pour toute aide.


Mise à jour - 2012: 07: 10T02: 34: 00Z

Il s'agit d'un code qui, pour un site Web donné, renverra la liste des CSS. Je veux tester s'il renvoie la bonne liste de 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)

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

À l'heure actuelle, le code dépend d'un serveur en ligne. Ça ne devrait pas. Je veux pouvoir ajouter de nombreux types de combinaisons de feuilles de style et tester le protocole, puis plus tard quelques options sur leur analyse, leurs combinaisons, etc.

33
karlcow

Le démarrage d'un serveur Web pour les tests unitaires n'est certainement pas une bonne pratique. Les tests unitaires doivent être simples et isolés, ce qui signifie qu'ils doivent éviter d'effectuer des opérations IO par exemple.

Si ce que vous voulez écrire sont vraiment des tests unitaires, vous devez créer vos propres entrées de test et également regarder objets fictifs . Python étant un langage dynamique, le mocking et le singe pathing sont des outils simples et puissants pour écrire des tests unitaires. En particulier, jetez un œil à l'excellent module Mock .

Test unitaire simple

Donc, si nous regardons votre exemple CssTests, vous essayez de tester que css.getCssUriList Est capable d'extraire toute la feuille de style CSS référencée dans un morceau de HTML que vous lui donnez. Ce que vous faites dans ce test unitaire particulier ne teste pas que vous pouvez envoyer une demande et obtenir une réponse d'un site Web, non? Vous voulez simplement vous assurer qu'étant donné du HTML, votre fonction retourne la liste correcte des URL CSS. Donc, dans ce test, vous n'avez clairement pas besoin de parler à un vrai serveur HTTP.

Je ferais quelque chose comme ceci:

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)    

Se moquer avec l'injection de dépendance

Maintenant, quelque chose de moins évident serait de tester à l'unité la méthode getContent() de votre classe core.HttpRequests. Je suppose que vous utilisez une bibliothèque HTTP et ne faites pas vos propres demandes au-dessus des sockets TCP.

Pour garder vos tests au niveau de l'unité , vous ne voulez rien envoyer sur le fil. Ce que vous pouvez faire pour éviter cela, c'est d'avoir des tests qui garantissent que vous utilisez correctement votre bibliothèque HTTP. Il s'agit de tester non pas le comportement de votre code mais plutôt la façon dont il interagit avec les autres objets qui l'entourent.

Une façon de le faire serait de rendre explicite la dépendance de cette bibliothèque: nous pouvons ajouter un paramètre au HttpRequests.__init__ Pour lui passer une instance du client HTTP de la bibliothèque. Supposons que j'utilise une bibliothèque HTTP qui fournit un objet HttpClient sur lequel nous pouvons appeler get(). Vous pourriez faire quelque chose comme:

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)

Nous avons rendu la dépendance explicite et l'exigence doit maintenant être satisfaite par l'appelant de HttpRequests: c'est ce qu'on appelle l'injection de dépendance (DI).

DI est très utile pour deux choses:

  1. cela évite les surprises lorsque votre code dépend secrètement d'un objet pour exister quelque part
  2. il permet d'écrire un test qui injecte différents types d'objets en fonction de l'objectif de ce test

Ici, nous pouvons utiliser un objet factice que nous donnerons à core.HttpRequests Et qu'il utilisera, sans le savoir, comme s'il s'agissait de la vraie bibliothèque. Après cela, nous pouvons tester que l'interaction s'est déroulée comme prévu.

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)

Nous avons maintenant testé que notre méthode get_content Interagit correctement avec notre bibliothèque HTTP. Nous avons défini les limites de notre objet HttpRequests et les avons testées, et c'est aussi loin que nous devrions aller au niveau du test unitaire. La demande est maintenant entre les mains de cette bibliothèque et ce n'est certainement pas le rôle de notre suite de tests unitaires de tester que la bibliothèque fonctionne comme prévu.

Patch de singe

Imaginez maintenant que nous décidions d'utiliser la grande bibliothèque de requêtes . Son API étant plus procédurale, elle ne présente pas d'objet à récupérer pour faire des requêtes HTTP. Au lieu de cela, nous importons le module et appelons sa méthode get.

Notre classe HttpRequests dans core.py Ressemblerait alors à quelque chose comme ceci:

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)

Plus de DI, alors maintenant, nous nous demandons:

  • Comment puis-je empêcher l'interaction réseau de se produire?
  • Comment puis-je vérifier que j'utilise correctement le module requests?

C'est là que vous pouvez utiliser un autre mécanisme fantastique, mais controversé, que les langages dynamiques offrent: patch de singe . Nous remplacerons, lors de l'exécution, le module requests par un objet que nous créons et que nous pouvons utiliser dans notre test.

Notre test unitaire ressemblera alors à quelque chose comme:

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)

Pour rendre ce processus moins verbeux, le module mock a un décorateur patch qui s'occupe de l'échafaudage. Il suffit alors d'écrire:

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

Conclusion

Il est très important de garder le test unitaire petit, simple, rapide et autonome. Un test unitaire qui dépend d'un autre serveur pour fonctionner n'est tout simplement pas un test unitaire. Pour vous aider, DI est une excellente pratique et les objets fantaisie sont un excellent outil.

Au début, il n'est pas facile de se familiariser avec le concept de simulation et comment les utiliser. Comme tout outil électrique, ils peuvent aussi exploser entre vos mains et vous faire par exemple croire que vous avez testé quelque chose alors qu'en réalité vous ne l'avez pas fait. Il est primordial de s'assurer que le comportement et les entrées/sorties des objets fictifs reflètent la réalité.

P.S.

Étant donné que nous n'avons jamais interagi avec un vrai serveur HTTP au niveau du test unitaire, il est important d'écrire des tests d'intégration qui garantiront que notre application est en mesure de parler au type de serveurs avec lesquels elle traitera dans la vie réelle. Nous pourrions le faire avec un serveur à part entière configuré spécialement pour les tests d'intégration, ou en écrire un artificiel.

71
Rodrigue