unitarias pruebas integracion hacer como python unit-testing http mocking monkeypatching

hacer - pruebas de integracion python



Cómo simular una solicitud HTTP en un escenario de prueba unitaria en Python (1)

Me gustaría incluir un servidor web para todas mis pruebas relacionadas con HTTP. No necesita ser muy sofisticado. Preferiría no ser dependiente de estar en línea. Entonces podría probar algunas opciones de mi programa.

  1. Comience el servidor
  2. Cree algunos recursos (URI) con tipos de mime apropiados, código de respuesta, etc.
  3. Ejecute las pruebas (sería bueno no tener que iniciar el servidor para cada prueba también)
  4. Apaga el servidor.

Cualquier sugerencia sobre este código sería útil. Intenté algunas cosas con BaseHTTPServer pero aún no fue exitoso. El comando nosetests parece esperar indefinidamente.

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

Gracias por cualquier ayuda.

Actualización - 2012: 07: 10T02: 34: 00Z

Este es un código que para un sitio web determinado devolverá la lista de CSS. Quiero probar si devuelve la lista correcta 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)

Luego, en 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

En este momento, el código depende de un servidor en línea. No debería. Quiero poder agregar muchos tipos diferentes de combinación de hojas de estilo y probar el protocolo y luego algunas opciones sobre su análisis sintáctico, combinaciones, etc.


Iniciar un servidor web para pruebas unitarias definitivamente no es una buena práctica. Las pruebas unitarias deben ser simples y aisladas, lo que significa que deben evitar realizar operaciones IO, por ejemplo.

Si lo que quieres escribir son pruebas unitarias, entonces debes crear tus propias entradas de prueba y también buscar objetos simulados . Python es un lenguaje dinámico, una burla y un mono son herramientas fáciles y potentes para escribir pruebas unitarias. En particular, eche un vistazo al excelente módulo Mock .

Prueba unitaria simple

Por lo tanto, si echamos un vistazo al ejemplo de CssTests , intentamos probar que css.getCssUriList puede extraer todas las hojas de estilo CSS a las que se hace referencia en un fragmento de HTML que le proporcione. Lo que está haciendo en esta prueba de unidad en particular no es probar que puede enviar una solicitud y obtener una respuesta de un sitio web, ¿verdad? Simplemente quiere asegurarse de que, dado algo de HTML, su función devuelve la lista correcta de URL de CSS. Entonces, en esta prueba, claramente no necesita hablar con un servidor HTTP real.

Haría algo como lo siguiente:

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)

Burlarse con Inyección de Dependencia

Ahora, algo menos obvio sería probar en unidades el método getContent() de su clase core.HttpRequests . Supongo que está utilizando una biblioteca HTTP y no hace sus propias solicitudes sobre los sockets TCP.

Para mantener sus pruebas en el nivel de la unidad , no desea enviar nada por el cable. Lo que puede hacer para evitar eso, es tener pruebas que aseguren que hace uso de su biblioteca HTTP correctamente. Se trata de probar no el comportamiento de su código, sino la forma en que interactúa con los otros objetos que lo rodean.

Una forma de hacerlo sería hacer explícita la dependencia de esa biblioteca: podemos agregar un parámetro a HttpRequests.__init__ para pasarle una instancia del cliente HTTP de la biblioteca. Digamos que uso una biblioteca HTTP que proporciona un objeto HttpClient al que podemos llamar get() . Podrías hacer algo como:

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)

Hemos hecho que la dependencia sea explícita y ahora el solicitante de HttpRequests debe cumplir los requisitos: esto se llama Inyección de Dependencia (DI).

DI es muy útil por dos cosas:

  1. evita sorpresas donde tu código confía secretamente en algún objeto para existir en algún lado
  2. permite que se escriba una prueba que inyecte diferentes tipos de objetos dependiendo del objetivo de esa prueba

Aquí, podemos usar un objeto simulado que le daremos a core.HttpRequests y que usará, sin saberlo, como si fuera la biblioteca real. Después de eso, podemos probar que la interacción se llevó a cabo como se esperaba.

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)

Ahora hemos probado que nuestro método get_content interactúa correctamente con nuestra biblioteca HTTP. Hemos definido los límites de nuestro objeto HttpRequests y los hemos probado, y esto es todo lo que deberíamos hacer al nivel de prueba de la unidad. La solicitud está ahora en la mano de esa biblioteca y ciertamente no es el rol de nuestro conjunto de pruebas unitarias probar que la biblioteca funcione como se espera.

Parche de mono

Ahora imagine que decidimos usar la gran biblioteca de solicitudes . Como su API es más procesal, no presenta un objeto que podamos aprovechar para realizar solicitudes HTTP. En cambio, importaríamos el módulo y llamaríamos a su método get .

Nuestra clase HttpRequests en core.py podría verse de la siguiente manera:

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)

No más DI, así que ahora, nos quedamos pensando:

  • ¿Cómo puedo evitar que la interacción de la red ocurra?
  • ¿Cómo puedo probar que utilizo el módulo de requests correctamente?

Aquí es donde puedes usar otro mecanismo fantástico, pero controvertido, que ofrecen los lenguajes dinámicos: parche de mono . Reemplazaremos, en tiempo de ejecución, el módulo de requests con un objeto que creemos y podamos usar en nuestra prueba.

Nuestra prueba de unidad se verá así:

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)

Para que este proceso sea menos detallado, el módulo mock tiene un decorador de patch que cuida el andamio. Entonces solo necesitamos escribir:

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

Conclusión

Es muy importante mantener la prueba unitaria pequeña, simple, rápida y autónoma. Una prueba de unidad que se basa en otro servidor para ejecutarse simplemente no es una prueba de unidad. Para ayudar con eso, DI es una gran práctica, y se burlan de los objetos una gran herramienta.

Al principio, no es fácil entender el concepto de simulacro y cómo usarlos. Como todas las herramientas eléctricas, también pueden explotar en tus manos y, por ejemplo, hacerte creer que has probado algo cuando en realidad no lo has hecho. Asegurarse de que el comportamiento y la entrada / salida de los objetos falsos refleja la realidad es primordial.

PD

Dado que nunca hemos interactuado con un servidor HTTP real en el nivel de prueba de la unidad, es importante escribir pruebas de integración que aseguren que nuestra aplicación pueda hablar con el tipo de servidores con los que tratará en la vida real. Podríamos hacer esto con una configuración de servidor totalmente desarrollada especialmente para Pruebas de Integración, o escribir una diseñada.