2011-06-23 18 views
41

Me gustaría implementar algunas pruebas de unidad en un Scrapy (raspador de pantalla/rastreador web). Como un proyecto se ejecuta a través del comando "scrapy crawl", puedo ejecutarlo a través de algo como nose. Dado que el scrapy está construido sobre la parte superior del twisted, ¿puedo usar el marco de pruebas de la unidad Trial? ¿Si es así, cómo? De lo contrario, me gustaría obtener nose trabajando.Prueba de Unidad de Curado

Actualización:

He estado hablando sobre Scrapy-Users y supongo que se supone que "la construcción de la respuesta en el código de prueba, y luego llamar al método con la respuesta y afirmar que [I] consigo los artículos/solicitudes esperadas en la salida ". Parece que no puedo hacer que esto funcione.

puedo construir una clase de prueba de la prueba unitaria y en una prueba:

  • crear un objeto respuesta
  • intento para llamar al método de análisis sintáctico de mi araña con el objeto respuesta

Sin embargo, termina generando this traceback. ¿Alguna idea de por qué?

Respuesta

44

La forma en que lo he hecho es crear respuestas falsas, de esta manera puede probar la función de análisis sin conexión. Pero obtienes la situación real usando HTML real.

Un problema con este enfoque es que su archivo HTML local puede no reflejar el último estado en línea. Entonces, si el HTML cambia en línea, es posible que tengas un gran error, pero tus casos de prueba aún pasarán. Entonces, puede que no sea la mejor manera de probarlo de esta manera.

Mi flujo de trabajo actual es que, siempre que haya un error, enviaré un correo electrónico a admin, con la url. Luego, para ese error específico, creo un archivo html con el contenido que está causando el error. Luego creo una prueba unitaria para ello.

Este es el código que utilizo para crear respuestas de la muestra Scrapy http para la prueba de un archivo HTML local:

# scrapyproject/tests/responses/__init__.py 

import os 

from scrapy.http import Response, Request 

def fake_response_from_file(file_name, url=None): 
    """ 
    Create a Scrapy fake HTTP response from a HTML file 
    @param file_name: The relative filename from the responses directory, 
         but absolute paths are also accepted. 
    @param url: The URL of the response. 
    returns: A scrapy HTTP response which can be used for unittesting. 
    """ 
    if not url: 
     url = 'http://www.example.com' 

    request = Request(url=url) 
    if not file_name[0] == '/': 
     responses_dir = os.path.dirname(os.path.realpath(__file__)) 
     file_path = os.path.join(responses_dir, file_name) 
    else: 
     file_path = file_name 

    file_content = open(file_path, 'r').read() 

    response = Response(url=url, 
     request=request, 
     body=file_content) 
    response.encoding = 'utf-8' 
    return response 

El archivo HTML de ejemplo se encuentra en scrapyproject/pruebas/respuestas/OSDir/sample.html

A continuación, el caso de prueba podría ser como sigue: la ubicación caso de prueba es scrapyproject/pruebas/test_osdir.py

import unittest 
from scrapyproject.spiders import osdir_spider 
from responses import fake_response_from_file 

class OsdirSpiderTest(unittest.TestCase): 

    def setUp(self): 
     self.spider = osdir_spider.DirectorySpider() 

    def _test_item_results(self, results, expected_length): 
     count = 0 
     permalinks = set() 
     for item in results: 
      self.assertIsNotNone(item['content']) 
      self.assertIsNotNone(item['title']) 
     self.assertEqual(count, expected_length) 

    def test_parse(self): 
     results = self.spider.parse(fake_response_from_file('osdir/sample.html')) 
     self._test_item_results(results, 10) 

Eso es basica lly cómo pruebo mis métodos de análisis, pero no es solo para los métodos de análisis. Si se vuelve más complejo sugiero mirar Mox

+1

acercamiento agradable para las pruebas fuera de línea. ¿Qué sucede si ejecuta pruebas fuera de línea para asegurarse de que no tiene fallas en el código y luego ejecuta pruebas en línea para asegurarse de que los cambios en el sitio no interrumpan su programa? – Medeiros

+0

@Medeiros, esa es la forma en que lo estoy haciendo en otro proyecto en este momento. Etiqueto las pruebas con @ integration = 1 para no tener que ejecutar siempre todas las pruebas. Estoy haciendo esto con el plugin de etiquetado nosetests. –

+0

@SamStoelinga ¿Puedo también probar contra datos reales? Si es así, ¿cómo puedo "buscar" la respuesta usando scrapy dentro de la unidad de prueba? Me encantaría comprobar si mi araña aún reúne toda la información de un lado cambiado. – lony

1

Puede seguir this snippet desde el sitio scrapy para ejecutarlo desde un script. Luego puede hacer cualquier tipo de afirmaciones que desee sobre los artículos devueltos.

13

Merece la pena probar el recién agregado Spider Contracts. Le brinda una manera simple de agregar pruebas sin requerir mucho código.

+4

Es muy pobre en este momento. Debe escribir sus propios contratos para verificar algo más complicado que * el análisis de esta página devuelve N elementos con los campos 'foo' y' bar' rellenos con cualquier dato * –

+0

No cumple la función. Traté de cambiar mis selectores y forzar las respuestas vacías que todavía pasaron todos los contratos –

9

utilizo Betamax para ejecutar la prueba en el sitio real de la primera vez y tener respuestas HTTP a nivel local para que la próxima pruebas se ejecutan después de súper rápido:

Betamax intercepts every request you make and attempts to find a matching request that has already been intercepted and recorded.

Cuando usted necesita para obtener la última versión del sitio, basta con retirar lo betamax ha registrado y vuelto a ejecutar la prueba.

Ejemplo:

from scrapy import Spider, Request 
from scrapy.http import HtmlResponse 


class Example(Spider): 
    name = 'example' 

    url = 'http://doc.scrapy.org/en/latest/_static/selectors-sample1.html' 

    def start_requests(self): 
     yield Request(self.url, self.parse) 

    def parse(self, response): 
     for href in response.xpath('//a/@href').extract(): 
      yield {'image_href': href} 


# Test part 
from betamax import Betamax 
from betamax.fixtures.unittest import BetamaxTestCase 


with Betamax.configure() as config: 
    # where betamax will store cassettes (http responses): 
    config.cassette_library_dir = 'cassettes' 
    config.preserve_exact_body_bytes = True 


class TestExample(BetamaxTestCase): # superclass provides self.session 

    def test_parse(self): 
     example = Example() 

     # http response is recorded in a betamax cassette: 
     response = self.session.get(example.url) 

     # forge a scrapy response to test 
     scrapy_response = HtmlResponse(body=response.content, url=example.url) 

     result = example.parse(scrapy_response) 

     self.assertEqual({'image_href': u'image1.html'}, result.next()) 
     self.assertEqual({'image_href': u'image2.html'}, result.next()) 
     self.assertEqual({'image_href': u'image3.html'}, result.next()) 
     self.assertEqual({'image_href': u'image4.html'}, result.next()) 
     self.assertEqual({'image_href': u'image5.html'}, result.next()) 

     with self.assertRaises(StopIteration): 
      result.next() 

FYI, descubrir Betamax en PyCon 2015 gracias a Ian Cordasco's talk.

+0

Lo usé. Betamax es genial –

0

Estoy usando Twisted's trial para ejecutar pruebas, similar a las propias pruebas de Scrapy. Ya inicia un reactor, por lo que utilizo el CrawlerRunner sin preocuparme por iniciar y detener uno en las pruebas.

Stealing algunas ideas de la check y parse Scrapy comandos que terminó con la siguiente clase base TestCase para funcionar afirmaciones contra los sitios en vivo:

from twisted.trial import unittest 

from scrapy.crawler import CrawlerRunner 
from scrapy.http import Request 
from scrapy.item import BaseItem 
from scrapy.utils.spider import iterate_spider_output 

class SpiderTestCase(unittest.TestCase): 
    def setUp(self): 
     self.runner = CrawlerRunner() 

    def make_test_class(self, cls, url): 
     """ 
     Make a class that proxies to the original class, 
     sets up a URL to be called, and gathers the items 
     and requests returned by the parse function. 
     """ 
     class TestSpider(cls): 
      # This is a once used class, so writing into 
      # the class variables is fine. The framework 
      # will instantiate it, not us. 
      items = [] 
      requests = [] 

      def start_requests(self): 
       req = super(TestSpider, self).make_requests_from_url(url) 
       req.meta["_callback"] = req.callback or self.parse 
       req.callback = self.collect_output 
       yield req 

      def collect_output(self, response): 
       try: 
        cb = response.request.meta["_callback"] 
        for x in iterate_spider_output(cb(response)): 
         if isinstance(x, (BaseItem, dict)): 
          self.items.append(x) 
         elif isinstance(x, Request): 
          self.requests.append(x) 
       except Exception as ex: 
        print("ERROR", "Could not execute callback: ",  ex) 
        raise ex 

       # Returning any requests here would make the  crawler follow them. 
       return None 

     return TestSpider 

Ejemplo:

@defer.inlineCallbacks 
def test_foo(self): 
    tester = self.make_test_class(FooSpider, 'https://foo.com') 
    yield self.runner.crawl(tester) 
    self.assertEqual(len(tester.items), 1) 
    self.assertEqual(len(tester.requests), 2) 

o realizar un pedido en la configuración y ejecutar múltiples pruebas contra los resultados:

@defer.inlineCallbacks 
def setUp(self): 
    super(FooTestCase, self).setUp() 
    if FooTestCase.tester is None: 
     FooTestCase.tester = self.make_test_class(FooSpider, 'https://foo.com') 
     yield self.runner.crawl(self.tester) 

def test_foo(self): 
    self.assertEqual(len(self.tester.items), 1) 
1

estoy usando scrapy 1.3.0 y la función: fake_response_from_file, generará un error:

response = Response(url=url, request=request, body=file_content) 

me sale:

raise AttributeError("Response content isn't text") 

La solución es utilizar TextResponse lugar, y funciona bien , como ejemplo:

response = TextResponse(url=url, request=request, body=file_content)  

Muchas gracias.

+0

'response.encoding = 'utf-8'' tiene que ser eliminado también. –

0

un poco más sencillo, mediante la eliminación de la def fake_response_from_file de la respuesta elegida:

import unittest 
from spiders.my_spider import MySpider 
from scrapy.selector import Selector 


class TestParsers(unittest.TestCase): 


    def setUp(self): 
     self.spider = MySpider(limit=1) 
     self.html = Selector(text=open("some.htm", 'r').read()) 


    def test_some_parse(self): 
     expected = "some-text" 
     result = self.spider.some_parse(self.html) 
     self.assertEqual(result, expected) 


if __name__ == '__main__': 
    unittest.main()