Scrapy Unit Testing - PullRequest
       13

Scrapy Unit Testing

53 голосов
/ 23 июня 2011

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

Обновление:

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

Я могу построить тестовый класс юнит-теста и в тесте:

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

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

Ответы [ 8 ]

61 голосов
/ 05 октября 2012

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

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

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

Это код, который я использую для создания образцов ответов Scrapy http для тестирования из локального 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 / response / 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)

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

19 голосов
/ 05 октября 2012

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

12 голосов
/ 06 июля 2016

Я использую 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()

FYI,Я обнаружил betamax на pycon 2015 благодаря лекции Иана Кордаско .

3 голосов
/ 02 января 2017

Я использую 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)     

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

2 голосов
/ 09 февраля 2018

Немного проще, удалив 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()
1 голос
/ 25 февраля 2019

Это очень поздний ответ, но меня раздражает тестирование 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 для обнаружения изменений веб-сайта.

1 голос
/ 13 октября 2016

Я использую 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)
1 голос
/ 27 июня 2011

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

...