70

Scrapy (スクリーン スクレーパー/Web クローラー) でいくつかの単体テストを実装したいと思います。プロジェクトは「スクレイピー クロール」コマンドで実行されるため、nose などで実行できます。スクレイピーはツイストの上に構築されているため、単体テスト フレームワークのトライアルを使用できますか? もしそうなら、どのように?それ以外の場合は、鼻を動かしたいと思います。

アップデート:

私はScrapy-Usersについて話してきましたが、「テスト コードでレスポンスを作成し、次にレスポンスでメソッドを呼び出し、[I] 出力で期待されるアイテム/リクエストを取得する」ことをアサートすることになっていると思います。私はこれを機能させることができないようです。

単体テスト テスト クラスとテストを作成できます。

  • 応答オブジェクトを作成する
  • 応答オブジェクトを使用してスパイダーの parse メソッドを呼び出してみてください

ただし、最終的にこのトレースバックが生成されます。理由についての洞察はありますか?

4

10 に答える 10

77

私が行った方法は、偽の応答を作成することです。この方法で、解析機能をオフラインでテストできます。しかし、実際の HTML を使用することで実際の状況を把握できます。

このアプローチの問題は、ローカルの HTML ファイルがオンラインの最新の状態を反映していない可能性があることです。したがって、HTML がオンラインで変更された場合、大きなバグが発生する可能性がありますが、テスト ケースはパスします。したがって、この方法でテストするのは最善の方法ではないかもしれません。

私の現在のワークフローは、エラーが発生するたびに、URL を記載したメールを管理者に送信するというものです。次に、その特定のエラーに対して、エラーの原因となっているコンテンツを含む html ファイルを作成します。次に、ユニットテストを作成します。

これは、ローカルの html ファイルからテストするためのサンプルの Scrapy http 応答を作成するために使用するコードです。

# 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/responses/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を検討することをお勧めします

于 2012-10-05T06:51:32.237 に答える
26

Betamaxを使用して、実際のサイトで初めてテストを実行し、http 応答をローカルに保持して、次のテストが超高速で実行されるようにします。

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

参考までに、 Ian Cordasco の講演のおかげで、pycon 2015 で betamax を発見しました。

于 2016-07-05T23:00:22.137 に答える
23

新しく追加されたスパイダー コントラクトは試す価値があります。多くのコードを必要とせずにテストを追加する簡単な方法を提供します。

于 2012-10-05T18:01:30.087 に答える
4

trialScrapy 自身のテストと同様に、テストを実行するためにTwisted を使用しています。すでにリアクターが起動しているのでCrawlerRunner、テストでの起動と停止を気にせずに使用しています。

checkおよびparseScrapy コマンドからいくつかのアイデアを盗み、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)

または、セットアップで 1 つの要求を実行し、結果に対して複数のテストを実行します。

@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)
于 2016-10-13T12:06:48.430 に答える
3

私はスクレイピー 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)     

どうもありがとう。

于 2017-01-02T15:54:51.900 に答える
2

スクレイピー サイトからこのスニペットをたどって、スクリプトから実行できます。次に、返されたアイテムに対して任意の種類のアサートを行うことができます。

于 2011-06-27T14:16:12.667 に答える
1

Hadrien の回答に似ていますが、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

于 2020-11-05T20:27:23.233 に答える