• 对scrapy进行单元测试 -- 使用betamax


    对于scrapy的单元测试,官方文档并没有提到,只是说有一个Contract功能。但是相信我,这个东西真的不好用,甚至scrapy的作者在一个issue中都说到希望删去这个功能。

    那么scrapy应该怎么测试呢?

    首先我们要明白我们真正想测试的是什么:

    • 我们不是要测试爬虫是否能访问站点!这个应该在你编写爬虫的时候就做到;如果你的代码在运行突然不可以访问站点了,也应该使用sentry这种日志监控系统。
    • 我们要测试parse(), parse_xx()方法是否如预期返回想要的item和request
    • 我们要测试parse()返回的item中字段类型是否正确。尤其是你用了scrapy的processor系统之后

    使用betamax进行单元测试

    关于betamax的介绍,可以看我的这篇博客

    我们实际要做的不仅是单元测试1,还是集成测试2。我们不想每次都重复进行真实的请求,我们不想使用啰嗦的mock

    爬虫代码

    下面是我们的爬虫代码,这是爬取一个ip代理网站,获取最新发布的ip:

    # src/spider.py
    import scrapy
    from scrapy.loader import ItemLoader
    from scrapy.loader.processors import TakeFirst, MapCompose, Join
    
    
    class IPItem(scrapy.Item):
        ip = scrapy.Field(
            input_processor=MapCompose(str, str.strip),
            output_processor=TakeFirst()
        )
        port = scrapy.Field(
            input_processor=MapCompose(str, str.strip),
            output_processor=TakeFirst()
        )
        protocol = scrapy.Field(
            input_processor=MapCompose(str, str.strip, str.lower),
            output_processor=TakeFirst()
        )
        remark = scrapy.Field(
            input_processor=MapCompose(str, str.strip),
            output_processor=Join(separator=', ')
        )
        source = scrapy.Field(
            input_processor=MapCompose(str, str.strip),
            output_processor=TakeFirst()
        )
        
    
    class IpData5uSpider(scrapy.Spider):
        name = 'ip-data5u'
        allowed_domains = ['data5u.com']
        start_urls = [
            'http://www.data5u.com/free/index.shtml',
            'http://www.data5u.com/free/gngn/index.shtml',
        ]
        custom_settings = {
            'USER_AGENT': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36',
            'DOWNLOAD_DELAY': 1
        }
    
        def parse(self, response):
            for row in response.css('div.wlist ul.l2'):
                loader = ItemLoader(item=IPItem(), selector=row)
                loader.add_value('source', 'data5u')
                loader.add_css('ip', 'span:nth-child(1) li::text')
                loader.add_css('port', 'span:nth-child(2) li::text')
                loader.add_css('protocol', 'span:nth-child(4) li::text')
                loader.add_css('remark', 'span:nth-child(5) li::text')
                loader.add_css('remark', 'span:nth-child(5) li::text')
                yield loader.load_item()
    

    测试代码

    我们使用pytest编写项目的单元测试,首先我们编写一些fixture函数:

    # tests/conftest.py
    import pathlib
    import pytest
    from scrapy.http import HtmlResponse, Request
    
    import betamax
    from betamax.fixtures.pytest import _betamax_recorder
    
    # betamax配置,设置betamax录像带的存储位置
    cassette_dir = pathlib.Path(__file__).parent / 'fixture' / 'cassettes'
    cassette_dir.mkdir(parents=True, exist_ok=True)
    with betamax.Betamax.configure() as config:
        config.cassette_library_dir = cassette_dir.resolve()
        config.preserve_exact_body_bytes = True
    
    
    @pytest.fixture
    def betamax_recorder(request):
        """修改默认的betamax pytest fixtures
        让它默认可用接口pytest.mark.parametrize装饰器,并且生产不同的录像带.
        有些地方可能会用到
        """
        return _betamax_recorder(request, parametrized=True)
    
    
    @pytest.fixture
    def resource_get(betamax_session):
        """这是一个pytest fixture
        返回一个http请求方法,相当于:
        
        with Betamax(session) as vcr:
            vcr.use_use_cassette('这里是测试函数的qualname')
            resp = session.get(url, *args, **kwargs)
            # 将requests的Response,封装成scrapy的HtmlResponse
            return HtmlResponse(body=resp.content)
        """
        def get(url, *args, **kwargs):
            request = kwargs.pop('request', None)
            resp = betamax_session.get(url, *args, **kwargs)
            selector = HtmlResponse(body=resp.content, url=url, request=request)
            return selector
    
        return get
    

    然后是测试函数:

    # tests/test_spider/test_ip_spider.py
    from src.spider import IpData5uSpider, IPItem
    
    def test_proxy_data5u_spider(resource_get):
        spider = IpData5uSpider()
        headers = {
            'user-agent': spider.custom_settings['USER_AGENT']
        }
    
        for urlr in spider.start_urls:
            selector = resource_get(url, headers=headers, request=req)
    
            result = spider.parse(selector)
            for item in result:
                if isinstance(item, IPItem):
                    assert isinstance(item['port'], str)
                    assert item['ip']
                    assert item['protocol'] in ('http', 'https')
                elif isinstance(item, Request):
                    assert item.url.startswith(req.url)
                else:
                    raise ValueError('yield 输出了意料外的item')
    

    然后我们运行它:

    >>> pytest
    ...
    Results (2.12s):
           1 passed
    

    我们可以看到fixture目录出现新的文件,类似xxx.tests.test_spiders.test_ip_spider.test_proxy_data5u_spider.json这样的文件名.

    再运行一次:

    >>> pytest
    ...
    Results (0.56s):
           1 passed
    

    测试运行速度明显变快,这是因为这一次使用的是保存在fixture的文件,用它来代替进行真正的http request操作。

    另外我们可以看一下fixture中json文件的内容:

    {"http_interactions": [{"request": {"body": {"encoding": "utf-8", "base64_string": ""}, "headers": {"user-agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["*/*"], "Connection": ["keep-alive"]}, "method": "GET", "uri": "http://www.data5u.com/free/index.shtml"}, "response": {"body": {"encoding": "UTF-8", "base64_string": "H4sIAAAAAAx..."}]}
    

    可以看到这里保存了一个response的全部信息,通过这个response再构造一个request.Response也不是难事吧。这就是betamax的原理。

  • 相关阅读:
    事件节流函数封装层
    rem布局
    对象克隆2
    HTTP的请求头标签 If-Modified-Since
    java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver
    idea 内置tomcat jersey 跨服务器 上传文件报400错误
    idea 内置tomcat jersey 上传文件报403错误
    java 配置aop 写入无效
    java中AOP的环绕通知
    java的Test 如何使用@Autowired注解
  • 原文地址:https://www.cnblogs.com/thomaszdxsn/p/duiscrapy-jin-xing-dan-yuan-ce-shi--shi-yongbetama.html
Copyright © 2020-2023  润新知