对于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的原理。

对scrapy进行单元测试 -- 使用betamax的更多相关文章

  1. Learning Scrapy笔记(三)- Scrapy基础

    摘要:本文介绍了Scrapy的基础爬取流程,也是最重要的部分 Scrapy的爬取流程 Scrapy的爬取流程可以概括为一个方程式:UR2IM,其含义如下图所示 URL:Scrapy的运行就从那个你想要 ...

  2. python爬虫scrapy项目详解(关注、持续更新)

    python爬虫scrapy项目(一) 爬取目标:腾讯招聘网站(起始url:https://hr.tencent.com/position.php?keywords=&tid=0&st ...

  3. scrapy将爬取到的数据存入elasticsearch

    pip安装 elasticsearch-dsl的包, 是elasticsearch提供给python 的接口 if __name__ == "__main__": 这个用来调试,还 ...

  4. scrapy爬取cnblogs文章列表

    scrapy爬取cnblogs文章 目标任务 安装爬虫 创建爬虫 编写 items.py 编写 spiders/cnblogs.py 编写 pipelines.py 编写 settings.py 运行 ...

  5. Scrapy框架爬虫初探——中关村在线手机参数数据爬取

    关于Scrapy如何安装部署的文章已经相当多了,但是网上实战的例子还不是很多,近来正好在学习该爬虫框架,就简单写了个Spider Demo来实践.作为硬件数码控,我选择了经常光顾的中关村在线的手机页面 ...

  6. Intellij idea添加单元测试工具

    1.idea 版本是14.0.0 ,默认带有Junit,但是不能自动生成单元测试,需要下载JunitGererator2.0插件 2.Settings -Plugins,下载 JunitGenerat ...

  7. Python的单元测试(二)

    title: Python的单元测试(二) date: 2015-03-04 19:08:20 categories: Python tags: [Python,单元测试] --- 在Python的单 ...

  8. Python的单元测试(一)

    title: Python的单元测试(一) author: 青南 date: 2015-02-27 22:50:47 categories: Python tags: [Python,单元测试] -- ...

  9. scrapy爬虫docker部署

    spider_docker 接我上篇博客,为爬虫引用创建container,包括的模块:scrapy, mongo, celery, rabbitmq,连接https://github.com/Liu ...

随机推荐

  1. spark 三种数据集的关系(二)

    一个Dataset是一个分布式的数据集,而且它是一个新的接口,这个新的接口是在Spark1.6版本里面才被添加进来的,所以要注意DataFrame是先出来的,然后在1.6版本才出现的Dataset,提 ...

  2. 简单的c程序分析

    1.裸函数 c语言的普通函数中即使什么都不写,编译器在编译时也会给它加上一些汇编代码:比如开栈.返回等: 裸函数就是编译器什么都不管,一切都需要自己来处理: 裸函数的优点是自由度高,可以应用在钩子程序 ...

  3. 【CUDA 基础】3.6 动态并行

    title: [CUDA 基础]3.6 动态并行 categories: - CUDA - Freshman tags: - 动态并行 - 嵌套执行 - 隐式同步 toc: true date: 20 ...

  4. 【CUDA 基础】3.2 理解线程束执行的本质(Part I)

    title: [CUDA 基础]3.2 理解线程束执行的本质(Part I) categories: CUDA Freshman tags: 线程束分化 CUDA分支 toc: true date: ...

  5. 查找:find、locate、which、whereis

    有find . locate . which . whereis 一.find 命令格式:[root@localhost ~]# find 搜索路径 [选项] 搜索内容 find是比较特殊的命令,它有 ...

  6. DRL Hands-on book

    代码:https://github.com/PacktPublishing/Deep-Reinforcement-Learning-Hands-On Chapter 1 What is Reinfor ...

  7. flask-数据库 进阶

    1. 级联操作 Cascade意为“级联操作”,就是在操作一个对象的同时,对相关的对象也执行某些操作.我们通过一个Post模型和Comment模型来演示级联操作,分别表示文章(帖子)和评论,两者为一对 ...

  8. TCP发送窗口更新tcp_ack_update_window

    在tcp_ack接收ACK处理函数中,如果确认当前走慢速路径,那么会调用tcp_ack_update_window函数检查窗口是否需要更新并更新之,并且更新未确认数据的位置,即更新窗口左边沿: sta ...

  9. flask登录功能实现的思路

    flask登录实现过程思路:前端创建表单,post方法,userno,pwd参数 后端首先创建登录验证函数check(验证函数是指通过request.get来获取前端的userno,pwd.然后将两个 ...

  10. 石川es6课程---13-16、generator-认识生成器函数

    石川es6课程---13-16.generator-认识生成器函数 一.总结 一句话总结: ` generator函数,中间可以停,到哪停呢,用 yield 配合,交出执行权 ` 需要调用next() ...