对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的原理。
对scrapy进行单元测试 -- 使用betamax的更多相关文章
- Learning Scrapy笔记(三)- Scrapy基础
摘要:本文介绍了Scrapy的基础爬取流程,也是最重要的部分 Scrapy的爬取流程 Scrapy的爬取流程可以概括为一个方程式:UR2IM,其含义如下图所示 URL:Scrapy的运行就从那个你想要 ...
- python爬虫scrapy项目详解(关注、持续更新)
python爬虫scrapy项目(一) 爬取目标:腾讯招聘网站(起始url:https://hr.tencent.com/position.php?keywords=&tid=0&st ...
- scrapy将爬取到的数据存入elasticsearch
pip安装 elasticsearch-dsl的包, 是elasticsearch提供给python 的接口 if __name__ == "__main__": 这个用来调试,还 ...
- scrapy爬取cnblogs文章列表
scrapy爬取cnblogs文章 目标任务 安装爬虫 创建爬虫 编写 items.py 编写 spiders/cnblogs.py 编写 pipelines.py 编写 settings.py 运行 ...
- Scrapy框架爬虫初探——中关村在线手机参数数据爬取
关于Scrapy如何安装部署的文章已经相当多了,但是网上实战的例子还不是很多,近来正好在学习该爬虫框架,就简单写了个Spider Demo来实践.作为硬件数码控,我选择了经常光顾的中关村在线的手机页面 ...
- Intellij idea添加单元测试工具
1.idea 版本是14.0.0 ,默认带有Junit,但是不能自动生成单元测试,需要下载JunitGererator2.0插件 2.Settings -Plugins,下载 JunitGenerat ...
- Python的单元测试(二)
title: Python的单元测试(二) date: 2015-03-04 19:08:20 categories: Python tags: [Python,单元测试] --- 在Python的单 ...
- Python的单元测试(一)
title: Python的单元测试(一) author: 青南 date: 2015-02-27 22:50:47 categories: Python tags: [Python,单元测试] -- ...
- scrapy爬虫docker部署
spider_docker 接我上篇博客,为爬虫引用创建container,包括的模块:scrapy, mongo, celery, rabbitmq,连接https://github.com/Liu ...
随机推荐
- Mysql 5.6主从同步配置
主从同步,本质是利用数据库日志,将主库数据复制一份到从库,本质上是使用了数据复制技术. 本文概要 主库的基本配置 从库的基本配置 完全同步的步骤 注意事项 工作原理 1. 主库的基本配置 做两件事:启 ...
- 激活函数matlab代码
[转自:http://blog.csdn.net/fendoubasaonian/article/details/52974608] %JOINT Summary of this function g ...
- 学习shell(二)
条件分支: (条件表达式的中括号里面 空格不可以省略) = ] then echo '2 = 2'; else echo '2 != 2'; fi # 上面的代码不使用缩进, 并不会出错, 但不应该 ...
- java内存区域以及GC回收
参考资料: http://www.cnblogs.com/zhguang/p/3257367.html 概要: Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执 ...
- Javascript你必须要知道的知识点
1.使用 typeof bar === "object" 判断 bar 是不是一个对象有神马潜在的弊端?如何避免这种弊端? 使用 typeof 的弊端是显而易见的(这种弊端同使用 ...
- 数据结构图之三(最短路径--迪杰斯特拉算法——转载自i=i++
数据结构图之三(最短路径--迪杰斯特拉算法) [1]最短路径 最短路径?别乱想哈,其实就是字面意思,一个带边值的图中从某一个顶点到另外一个顶点的最短路径. 官方定义:对于内网图而言,最短路径是指两 ...
- 2016 Multi-University Training Contest 4 部分题解
1001,官方题解是直接dp,首先dp[i]表示到i位置的种类数,它首先应该等于dp[i-1],(假设m是B串的长度)同时,如果(i-m+1)这个位置开始到i这个位置的这一串是和B串相同的,那么dp[ ...
- Go 结构体与初始化
Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型. 结构体是复合类型,当需要定义类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在 ...
- 阿里云OSS细粒度权限控制
做下工作记录: 自定义安全策略,然后授权即可 { ", "Statement": [ { "Effect": "Allow", & ...
- tp5中很牛皮的一句sql语句,三个条件(两个不确定条件,一个硬性条件)
$result = Db::table('xxxxxx') // 表名 ->alias('g') ->join('xxxxx_2 u','g.user_id = u.id') -> ...