对于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. Pandas中DataFrame数据合并、连接(concat、merge、join)之merge

    二.merge:通过键拼接列 类似于关系型数据库的连接方式,可以根据一个或多个键将不同的DatFrame连接起来. 该函数的典型应用场景是,针对同一个主键存在两张不同字段的表,根据主键整合到一张表里面 ...

  2. BZOJ 4032: [HEOI2015]最短不公共子串 (dp*3 + SAM)

    转博客大法好 第4个子任务中,为什么只转移最近的一个位置,自己YY吧(多YY有益身体健康). #include <bits/stdc++.h> using namespace std; t ...

  3. SQL Server 删除日志文件

    -- 查询日志文件名,用于下面删除 USE [data_name] GO SELECT file_id, name FROM sys.database_files /*删除指定数据库的日志文件*/ U ...

  4. HDU 6050 - Funny Function | 2017 Multi-University Training Contest 2

    /* HDU 6050 - Funny Function [ 公式推导,矩阵快速幂 ] 题意: F(1,1) = F(1, 2) = 1 F(1,i) = F(1, i-1) + 2 * F(1, i ...

  5. Python 特点

    优点 简单 -- Python 是一种代表简单主义思想的语言.阅读一个良好的 Python 程序就感觉像是在读英语一样,尽管这个英语的要求非常严格!Python 的这种伪代码本质是它最大的优点之一.它 ...

  6. 快速拿下CSS盒子模型

    下面的图片就是Chrome浏览器审查元素里的盒子情况展示,我们可以看到一个容器由4个颜色代表的内容组成:内容(content).填充(padding).边框(border).边界(margin),在这 ...

  7. java内存区域以及GC回收

    参考资料: http://www.cnblogs.com/zhguang/p/3257367.html 概要: Java GC机制主要完成3件事:确定哪些内存需要回收,确定什么时候需要执行GC,如何执 ...

  8. 圆桌游戏(区间DP)

    2.圆桌游戏 (game.cpp/c/pas) [问题描述] 有一种圆桌游戏是这样进行的:n个人围着圆桌坐成一圈,按顺时针顺序依次标号为1号至n号.对1<=i<=n的i来说,i号的左边是i ...

  9. 2019.12.12网页设计大赛&2019.12.13程序设计大赛观后感

    有幸参加了一次网页设计大赛和程序设计大赛,其实在大一的时候就参加过一次程序设计大赛,那时候也没怎么听,现在又有了一次机会来听,这次就认真的听了这两次的比赛,也有很多的感悟. 1.要学习完成一个任务的多 ...

  10. AcWing:111. 畜栏预定(贪心 + 小根堆)

    有N头牛在畜栏中吃草. 每个畜栏在同一时间段只能提供给一头牛吃草,所以可能会需要多个畜栏. 给定N头牛和每头牛开始吃草的时间A以及结束吃草的时间B,每头牛在[A,B]这一时间段内都会一直吃草. 当两头 ...