Испытания блока Scrapy

Я хотел бы реализовать некоторые модульные тесты в Scrapy (скребок экрана / веб-сканер). Поскольку проект запускается с помощью команды "scrapy crawl", я могу запустить его чем-то вроде носа. Поскольку scrapy построен на основе twisted, могу ли я использовать его среду модульного тестирования Trial? Если да, то как? В противном случае я бы хотел, чтобы нос работал.

Обновление:

Я говорил о Scrapy-Users, и я предполагаю, что я должен "построить Ответ в тестовом коде, а затем вызовите метод с ответом и подтвердите, что [я] получил ожидаемые элементы / запросы на выходе ». Хотя я не могу заставить это работать.

Я могу создать тестовый класс unit-test и в тесте:

  • создать объект ответа
  • попробуйте вызвать метод синтаксического анализа моего паука с объектом ответа

Однако в конечном итоге он создает эту трассировку. Любое понимание того, почему?


person ciferkey    schedule 23.06.2011    source источник


Ответы (10)


Я сделал это так, чтобы создать фальшивые ответы, чтобы вы могли протестировать функцию синтаксического анализа в автономном режиме. Но реальную ситуацию можно получить, используя настоящий HTML.

Проблема с этим подходом заключается в том, что ваш локальный HTML-файл может не отражать последнее состояние в сети. Так что, если HTML изменяется онлайн, у вас может быть большая ошибка, но ваши тестовые примеры все равно пройдут. Так что это может быть не лучший способ проверить этот способ.

Мой текущий рабочий процесс: всякий раз, когда возникает ошибка, я отправляю электронное письмо администратору с URL-адресом. Затем для этой конкретной ошибки я создаю html-файл с содержимым, которое вызывает ошибку. Затем я создаю для него unittest.

Это код, который я использую для создания примеров HTTP-ответов Scrapy для тестирования из локального файла HTML:

# 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

Образец html-файла находится в scrapyproject / tests / answers / osdir / sample.html.

Тогда тестовый пример может выглядеть следующим образом: Местоположение тестового примера - 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)

В основном так я тестирую свои методы синтаксического анализа, но не только для методов синтаксического анализа. Если это станет более сложным, я предлагаю взглянуть на Mox.

person Sam Stoelinga    schedule 05.10.2012
comment
Хороший подход для автономного тестирования. Как насчет запуска автономных тестов, чтобы убедиться, что у вас нет недостатков в коде, а затем запускать онлайн-тесты, чтобы убедиться, что изменения сайта не нарушают вашу программу? - person Medeiros; 25.09.2013
comment
@Medeiros - вот как я сейчас делаю это в другом проекте. Я помечаю тесты тегом @ integration = 1, чтобы мне не приходилось всегда запускать все тесты. Я делаю это с помощью плагина для тегов. - person Sam Stoelinga; 04.02.2014
comment
@SamStoelinga Могу ли я также протестировать на реальных данных? Если да, то как я могу получить ответ с помощью scrapy внутри модульного теста? Я хотел бы проверить, собирает ли мой паук всю информацию с измененной стороны. - person lony; 07.02.2016
comment
Я сделал отдельный вопрос здесь. - person lony; 07.02.2016
comment
Я настоятельно рекомендую использовать Betamax для этого: stackoverflow.com/questions/ 6456304 / scrapy-unit-testing / - person Hadrien; 06.07.2016
comment
Это from scrapyproject.spiders import osdir_spider недействительно. Как я могу импортировать паука? - person ji-ruh; 18.08.2017
comment
@ ji-ruh ну, это зависит от того, где твой паук. Я предполагаю, что вы написали своего собственного паука, поэтому вам нужно изменить путь к своему собственному пауку ... Я предлагаю перечитать ответ и прочитать документы scrapy. Вы не можете просто скопировать и вставить этот ответ, вам нужно его понять. Также этот ответ от 5 лет назад, могут быть лучшие способы сделать это сегодня с помощью scrapy. - person Sam Stoelinga; 18.08.2017
comment
Я импортировал его таким образом. from articles.spiders.spidername import SpiderName. Благодарность - person ji-ruh; 21.08.2017
comment
Вы можете удалить def fake_response_from_file, выполнив from scrapy.selector import Selector, тогда ваша setUp функция будет иметь: self.fake_response = Selector(text=open(file, 'r').read()) - person b_dev; 09.02.2018
comment
В Python 3 относительный импорт не будет работать, если тестовый файл находится в дочернем каталоге. Если вы хотите это сделать, соберите свой проект в пакет, используя pip install -e my_pck_name - person Woody1193; 08.08.2018
comment
Если вы хотите добавить meta, добавьте его в создание Request экземпляра: request = Request(url=url, meta=meta) - person Nikolay Shindarov; 30.04.2019
comment
Это правильное решение, но лично я всегда считал тестирование соскоба пустой тратой времени. Возможно, я сильно заблуждаюсь здесь, но большую часть времени написание и выполнение тестов занимает столько же времени, сколько и создание сценария, и зависит от известной рабочей версии веб-сайта ... поэтому мне трудно понять, как тестирование изменений против известной рабочей версии это тест. - person eusid; 13.05.2019
comment
@eusid Я почувствовал, что моя продуктивность повысилась за счет суперкомпактного и простого теста, позволяющего быстро протестировать мой код парсинга. - person Sam Stoelinga; 31.12.2019
comment
Я знаю старую ветку, но как мне управлять отправкой элементов в конвейеры? - person Tobias Mayr; 22.01.2021

Я использую Betamax, чтобы запустить тест на реальном сайте в первый раз и сохранить http ответы локально, чтобы следующие тесты выполнялись очень быстро после:

Betamax перехватывает каждый ваш запрос и пытается найти соответствующий запрос, который уже был перехвачен и записан.

Когда вам нужно получить последнюю версию сайта, просто удалите то, что записано Betamax, и повторно запустите тест.

Пример:

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

К вашему сведению, я обнаружил Betamax на pycon 2015 благодаря Выступление Яна Кордаско.

person Hadrien    schedule 05.07.2016
comment
Было бы приятно узнать, как вы выполнили этот код? - person Baig; 27.06.2018

Стоит попробовать недавно добавленные контракты паука. Это дает вам простой способ добавлять тесты, не требуя большого количества кода.

person Shane Evans    schedule 05.10.2012
comment
В настоящий момент он очень плохой. Вы должны написать свои собственные контракты, чтобы проверить что-то более сложное, чем парсинг этой страницы возвращает N элементов с полями foo и bar, заполненными любыми данными - person Anton Egorov; 21.10.2013
comment
Это не служит цели. Я попытался изменить свои селекторы и заставить пустые ответы по-прежнему передавать все контракты - person Raheel; 24.02.2018

Это очень поздний ответ, но меня раздражало тестирование scrapy, поэтому я написал scrapy-test фреймворк для тестирования сканеров scrapy на соответствие определенным спецификациям.

Он работает путем определения тестовых спецификаций, а не статического вывода. Например, если мы сканируем такие элементы:

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

Мы можем определить 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')

Также есть те же тесты идей для статистики scrapy, что и StatsSpec:

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

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

Впоследствии его можно запускать для живых или кешированных результатов:

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

Я выполнял кэшированные прогоны для изменений в разработке и ежедневные cronjobs для обнаружения изменений на веб-сайтах.

person Granitosaurus    schedule 25.02.2019

Я использую Twisted trial для запуска тестов, похожих на собственные тесты Scrapy. Он уже запускает реактор, поэтому я использую CrawlerRunner, не беспокоясь о запуске и остановке реактора в тестах.

Позаимствовав некоторые идеи из команд check и parse Scrapy, я получил следующий базовый класс TestCase для выполнения утверждений на живых сайтах:

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

Пример:

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

или выполните один запрос в настройке и запустите несколько тестов по результатам:

@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)
person Aa'Koshh    schedule 13.10.2016

Чуть проще, убрав def fake_response_from_file из выбранного ответа:

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()
person b_dev    schedule 08.02.2018
comment
Это работает для меня, однако, если моя функция синтаксического анализа имеет проверку на response.url, она выдает ошибку, говоря 'Selector' object has no attribute 'url' - person addicted; 08.08.2020

Я использую scrapy 1.3.0 и функцию: fake_response_from_file, вызывает ошибку:

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

Я получил:

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

Решение состоит в том, чтобы использовать вместо этого TextResponse, и он работает нормально, например:

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

Большое спасибо.

person Kfeina    schedule 02.01.2017
comment
response.encoding = 'utf-8' также необходимо удалить. - person davegallant; 24.04.2017

Вы можете выполнить этот фрагмент с сайта scrapy, чтобы запустить его из сценария. Затем вы можете делать любые утверждения по возвращенным товарам.

person ciferkey    schedule 27.06.2011

https://github.com/ThomasAitken/Scrapy-Testmaster

Это пакет, который я написал, который значительно расширяет функциональность библиотеки Scrapy Autounit и развивает ее в другом направлении (позволяя легко динамически обновлять тестовые наборы и объединять процессы отладки / генерации тестов). Он также включает измененную версию команды Scrapy parse (https://docs.scrapy.org/en/latest/topics/commands.html#std-command-parse)

person Noam Hudson    schedule 22.05.2020
comment
Не могли бы вы объяснить немного больше? - person Dieter Meemken; 22.05.2020
comment
Короче говоря, идея состоит в том, что вы можете разработать собственные правила для проверки вашего вывода, а затем вы можете запускать одноразовые запросы к определенным URL-адресам или запускать своего полного паука, и он будет автоматически проверять результаты этих запросов на соответствие вашим пользовательским правилам. Если результаты соответствуют вашим пользовательским правилам, то создаются тестовые наборы, которые в будущем можно будет запускать статически, чтобы проверить, что изменения в вашем коде ничего не сломали. Кроме того, если вы хотите проверить, изменился ли веб-сайт, у вас также есть возможность воссоздать исходные запросы для создания новых тестовых сценариев. - person Noam Hudson; 26.05.2020

Подобно ответу Хадриена, но для pytest: pytest-vcr.

import requests
import pytest
from scrapy.http import HtmlResponse

@pytest.mark.vcr()
def test_parse(url, target):
    response = requests.get(url)
    scrapy_response = HtmlResponse(url, body=response.content)
    assert Spider().parse(scrapy_response) == target

person Jan-Benedikt Jagusch    schedule 05.11.2020