Контракты пауков

Пауки-тестеры могут стать особенно утомительными, и, хотя ничто не мешает вам писать модульные тесты, задача быстро становится громоздкой. Scrapy предлагает интегрированный способ тестирования ваших пауков посредством контрактов.

Это позволяет вам тестировать каждый обратный вызов вашего паука, жёстко кодируя образец URL и проверяя различные ограничения того, как обратный вызов обрабатывает ответ. У каждого контракта есть префикс @ и включается в строку документации. См. следующий пример:

def parse(self, response):
    """ This function parses a sample response. Some contracts are mingled
    with this docstring.

    @url http://www.amazon.com/s?field-keywords=selfish+gene
    @returns items 1 16
    @returns requests 0 0
    @scrapes Title Author Year Price
    """

Данный обратный вызов протестирован с использованием трёх встроенных контрактов:

class scrapy.contracts.default.UrlContract

Данный контракт (@url) устанавливает образец URL, используемый при проверке других условий контракта для этого паука. Данный контракт является обязательным. Все обратные вызовы, в которых отсутствует данный контракт, игнорируются при запуске проверок:

@url url
class scrapy.contracts.default.CallbackKeywordArgumentsContract

Данный контракт (@cb_kwargs) устанавливает атрибут cb_kwargs для образца запроса. Это должен быть действующий словарь JSON.

@cb_kwargs {"arg1": "value1", "arg2": "value2", ...}
class scrapy.contracts.default.ReturnsContract

Данный контракт (@returns) устанавливает нижнюю и верхнюю границы для элементов и запросов, возвращаемых пауком. Верхняя граница не обязательна:

@returns item(s)|request(s) [min [max]]
class scrapy.contracts.default.ScrapesContract

Данный контракт (@scrapes) проверяет, что все элементы, возвращаемые обратным вызовом, имеют указанные поля:

@scrapes field_1 field_2 ...

Используйте команду check для запуска проверок контракта.

Пользовательские контракты

Если вы обнаружите, что вам нужно больше мощности, чем встроенные контракты Scrapy, вы можете создать и загрузить свои собственные контракты в проект, используя параметр SPIDER_CONTRACTS:

SPIDER_CONTRACTS = {
    'myproject.contracts.ResponseCheck': 10,
    'myproject.contracts.ItemValidate': 10,
}

Каждый контракт должен наследовать от Contract и может переопределять три метода:

class scrapy.contracts.Contract(method, *args)
Параметры
  • method (collections.abc.Callable) – функция обратного вызова, с которой связан контракт

  • args (list) – список аргументов, переданных в строку документации (разделенные пробелами)

adjust_request_args(args)

Он получает dict в качестве аргумента, содержащего аргументы по умолчанию для объекта запроса. По умолчанию используется Request, но его можно изменить с помощью атрибута request_cls. Если для нескольких контрактов в цепочке определён данный атрибут, используется последний из них.

Должен возвращать ту же или модифицированную версию.

pre_process(response)

Позволяет выполнять различные проверки ответа, полученного от образца запроса, до его передачи в обратный вызов.

post_process(output)

Позволяет обрабатывать вывод обратного вызова. Итераторы преобразуются в списки перед тем, как быть переданы в этот хук.

Вызывается ContractFail из pre_process или post_process, если ожидания не оправдаются:

Вот демонстрационный контракт, который проверяет наличие настраиваемого заголовка в полученном ответе:

from scrapy.contracts import Contract
from scrapy.exceptions import ContractFail

class HasHeaderContract(Contract):
    """ Demo contract which checks the presence of a custom header
        @has_header X-CustomHeader
    """

    name = 'has_header'

    def pre_process(self, response):
        for header in self.args:
            if header not in response.headers:
                raise ContractFail('X-CustomHeader not present')

Обнаружение контрольных прогонов

Когда scrapy check работает, переменная среды SCRAPY_CHECK устанавливается равной строке true. Вы можете использовать os.environ для внесения любых изменений в ваши пауки или настройки, когда используется scrapy check:

import os
import scrapy

class ExampleSpider(scrapy.Spider):
    name = 'example'

    def __init__(self):
        if os.environ.get('SCRAPY_CHECK'):
            pass  # Do some scraper adjustments when a check is running