web-dev-qa-db-fra.com

Tests unitaires à la casse

Je voudrais mettre en œuvre des tests unitaires dans un Scrapy (grattoir d'écran/robot d'indexation Web). Puisqu'un projet est exécuté via la commande "scrapy crawl", je peux le faire par le biais de quelque chose comme le nez. Depuis que scrapy est construit sur du tordu, puis-je utiliser son framework de test unitaire Trial? Si c'est le cas, comment? Sinon, j'aimerais bien que (nez) travaille.

Mettre à jour:

J'ai parlé sur Scrapy-Users et je suppose que je suis censé "construire la réponse dans le code de test, puis appeler la méthode avec la réponse et affirmer que [j'ai] obtenir les éléments/demandes attendus le résultat". Je n'arrive pas à obtenir que cela fonctionne.

Je peux construire une classe de tests unitaires et dans un test:

  • créer un objet de réponse
  • essayez d'appeler la méthode d'analyse de mon araignée avec l'objet réponse

Cependant, il finit par générer this traceback. Une idée pour pourquoi?

50
ciferkey

La façon dont je l'ai fait est de créer de fausses réponses. Vous pouvez ainsi tester la fonction d'analyse en mode hors connexion. Mais vous obtenez la situation réelle en utilisant du vrai HTML.

Un problème avec cette approche est que votre fichier HTML local peut ne pas refléter l'état le plus récent en ligne. Donc, si le code HTML change en ligne, vous risquez d'avoir un gros problème, mais vos scénarios de test seront quand même réussis. Donc, ce n'est peut-être pas la meilleure façon de tester de cette façon.

Mon flux de travail actuel est, chaque fois qu'il y a une erreur je vais envoyer un email à admin, avec l'URL. Ensuite, pour cette erreur spécifique, je crée un fichier html avec le contenu à l'origine de l'erreur. Ensuite, je crée un paresseux pour cela.

C’est le code que j’utilise pour créer des exemples de réponses http Scrapy à des fins de test à partir d’un fichier 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

Le fichier HTML exemple se trouve dans scrapyproject/tests/answers/osdir/sample.html

Le cas de test peut alors ressembler à ceci: L'emplacement du cas de test est scrapyproject/tests/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)

C'est comme ça que je teste mes méthodes d'analyse, mais pas seulement pour les méthodes d'analyse. Si cela devient plus complexe, je suggère de regarder Mox

55
Sam Stoelinga

Les nouveaux contrats Spider Contracts valent la peine d’être essayés. Cela vous donne un moyen simple d'ajouter des tests sans nécessiter beaucoup de code.

17
Shane Evans

J'utilise Betamax pour exécuter un test sur un site réel la première fois et conserve les réponses http localement afin que les prochains tests fonctionnent très rapidement après: 

Betamax intercepte chaque demande que vous faites et tente de trouver une demande correspondante qui a déjà été interceptée et enregistrée. 

Lorsque vous avez besoin de la dernière version du site, supprimez ce que betamax a enregistré et relancez le test.

Exemple:

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()

Pour votre information, je découvre que betamax à pycon 2015 grâce à le discours de Ian Cordasco .

12
Hadrien

J'utilise scrapy 1.3.0 et la fonction: fake_response_from_file, génère une erreur: 

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

Je reçois: 

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

La solution consiste à utiliser TextResponse à la place, et cela fonctionne bien, à titre d'exemple: 

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

Merci beaucoup. 

2
Kfeina

Vous pouvez suivre this snippet à partir du site de scrapy pour l’exécuter à partir d’un script. Ensuite, vous pouvez faire n'importe quelle assertion que vous souhaitez sur les articles retournés.

1
ciferkey

J'utilise la variable trial de Twisted pour exécuter des tests, similaires aux propres tests de Scrapy. Il démarre déjà un réacteur, alors je me sers de la CrawlerRunner sans me soucier de démarrer et d’arrêter un réacteur dans les tests. 

En volant quelques idées des commandes check et parse Scrapy, je me suis retrouvé avec la classe de base TestCase suivante pour exécuter des assertions contre des sites en direct:

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

Exemple:

@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)

ou effectuez une requête dans la configuration et exécutez plusieurs tests sur les résultats:

@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
Aa'Koshh

Un peu plus simple, en supprimant le def fake_response_from_file de la réponse choisie:

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 __== '__main__':
    unittest.main()
1
b_dev

C’est une réponse très tardive, mais comme les tests sur scrapy me dérangent, j’ai donc écrit scrapy-test un framework pour tester les robots d'exploration sur des spécifications définies.

Cela fonctionne en définissant des spécifications de test plutôt qu'une sortie statique . Par exemple, si nous analysons ce type d'élément:

{
    "name": "Alex",
    "age": 21,
    "gender": "Female",
}

Nous pouvons définir scrapy-test ItemSpec:

from scrapytest.tests import Match, MoreThan, LessThan
from scrapytest.spec import ItemSpec

class MySpec(ItemSpec):
    name_test = Match('{3,}')  # name should be at least 3 characters long
    age_test = Type(int), MoreThan(18), LessThan(99)
    gender_test = Match('Female|Male')

Il existe également les mêmes tests d'idée pour les statistiques effrayantes que StatsSpec:

from scrapytest.spec import StatsSpec
from scrapytest.tests import Morethan

class MyStatsSpec(StatsSpec):
    validate = {
        "item_scraped_count": MoreThan(0),
    }

Ensuite, il peut être exécuté sur des résultats en direct ou en cache:

$ scrapy-test 
# or
$ scrapy-test --cache

J'ai exécuté des exécutions en cache pour les modifications de développement et des tâches quotidiennes pour détecter les modifications de sites Web.

0
Granitosaurus