基于Scrapy的艺术品爬虫项目测试相关技术咨询
问题背景与疑惑
我正在开发一个个人作品集项目,目标是构建一个可在线查看的最全艺术品(绘画、雕塑等)合集。很多博物馆公开高清图片和元数据,所以我先做了基础版本:一个爬取大都会博物馆官网数据的Scrapy爬虫,目前能获取搜索结果前3页艺术品的下载链接和名称。
我刚学测试,读了《Software Engineering at Google》里的测试章节,但没法把书中内容和项目结合。以下是相关代码,以及我为parse()方法写的测试代码:
爬虫代码(metQueryScraper.py)
class MetSpider(scrapy.Spider): name = 'metQueryScraper' params = { "offset": "0" } url = "https://www.metmuseum.org/art/collection/search?showOnly=openAccess&" start_urls = [url] async def start(self): temp_url = self.url + urlencode(self.params) yield scrapy.Request(url = temp_url, callback = self.parse) def parse(self, response): relativeURLs = response.xpath("//figure[contains(@class, 'collection-object')]/div[contains(@class, 'collection-object-module')]/a[contains(@href, '/art/collection/search')]") # 选择包含艺术品链接的DOM元素 # 为每个艺术品对象发起请求 for url in relativeURLs: artObject = ArtObject(title = url.xpath("@title").get()) temp_url = "https://www.metmuseum.org" + url.xpath("@href").get() yield scrapy.Request(url = temp_url, callback = self.parseObjectDownloadLink, meta = {"artObject": artObject}) nextPage = response.xpath("//div[contains(@class, 'pagination-controls')]/button[contains(@aria-label, 'Next Page')]/@aria-label").get() if(nextPage is not None and self.params["offset"] != "80"): # 限制爬虫爬取页数 self.params["offset"] = str(int(self.params["offset"]) + 40) temp_url = self.url + urlencode(self.params) yield scrapy.Request(url = temp_url, callback = self.parse) def parseObjectDownloadLink(self, response): # 获取图片下载链接 downloadLink = response.xpath("//figure[contains(@itemtype, 'ImageObject')]/img[contains(@fetchpriority, 'high')]") artObject = response.meta.get("artObject") artObject["link"] = downloadLink.xpath("@src").get() yield artObject
测试代码(test_metQueryScraper.py)
from betamax.fixtures.unittest import BetamaxTestCase from spiders.metQueryScraper import MetSpider from scrapy.http import HtmlResponse import os class TestmetQueryScraper(BetamaxTestCase): def test_shouldCreateAFileContainingNamesAndLinksOfArtObjects(self): # 准备:MetSpider实例 spider = MetSpider() with open("./vcr/cassettes/artwork-recorded-session.json", "rb") as file: fileContents = file.read() response = HtmlResponse(url = spider.url + '0', body=fileContents) # 执行:调用parse方法 spider.parse(response=response) # 断言:检查文件是否有内容 fileEmptyOrNot = os.stat("../../../artworkData.jsonl").st_size == 0 self.assertEqual(fileEmptyOrNot, True, "Expected contents to be added to file. File is currently empty" )
我用Betamax录制网络请求到JSON文件来模拟请求,避免每次测试都发真实请求,觉得这属于中等规模测试(不发真实请求但访问文件系统)。现在有几个疑惑:
- 这个测试是中等范围的集成测试还是窄范围的单元测试?它测试单个方法,但依赖Scrapy内置方法,让我不确定。
- 延伸问题:项目大部分基于Scrapy,自定义方法都会用到Scrapy或其他库的方法,我不清楚怎么定义单元测试,好像大部分测试都是集成或端到端测试,这是不是说明我的代码结构有问题?
- 项目要向多个博物馆发大量请求并存储数据,用录制请求存JSON的方式合适吗?有没有更好的方法测试这类操作并使用真实模拟数据?
- 我想让测试可维护,尽量清晰简洁、关注行为而非实现细节,不知道方向对不对。
希望得到针对这类项目测试的建议,谢谢!
解答
针对你的问题和代码,逐一拆解解答:
1. 测试类型判断
你的测试属于集成测试,原因:
- 单元测试的核心是隔离被测代码的依赖,只验证自身逻辑。但你的测试直接依赖Scrapy的
HtmlResponse,且parse方法本身就和Scrapy的响应处理、Request生成深度绑定,没有做依赖隔离。 - 它测试的是
parse方法和Scrapy响应系统的协作逻辑,而非纯自定义逻辑,符合集成测试的范畴(测试多个组件/库的协作)。
2. 基于框架的项目如何做单元测试?代码结构有问题吗?
不是代码结构的问题,而是框架类项目的单元测试思路要调整:
- 拆分纯逻辑代码:把和Scrapy无关的逻辑抽出来,比如艺术品元数据的清洗、分页偏移量的计算规则等。这些纯函数可以做单元测试,完全隔离框架依赖。
- 对Scrapy绑定的方法做“轻量集成测试”:像
parse、parseObjectDownloadLink这类和Scrapy响应/请求强绑定的方法,不需要硬拆成单元测试,而是用你现在的方式(模拟响应)做集成测试即可。 - 框架类项目本来就会有更多集成/端到端测试,这是正常的,不用强行追求单元测试覆盖率,重点是覆盖核心业务逻辑。
3. 录制网络请求的方式是否合适?有没有更好的方案?
你用Betamax的方式是合适且推荐的,针对爬虫这类依赖外部API/网页的项目,录制真实响应是最贴近生产环境的测试方式。另外可以补充这些优化:
- 定期更新录制的响应:博物馆网站可能会改页面结构,每隔一段时间重新录制一次,避免测试用例因为页面更新而失效。
- 使用最小化的测试响应:如果录制的响应太大,可以手动裁剪掉无关的HTML内容,只保留
parse方法需要的DOM节点,减少测试文件体积。 - 备选方案:手动构造模拟响应:如果某些场景很难录制(比如分页边界、错误页面),可以手动写极简的HTML作为响应,测试极端情况。
4. 测试关注行为而非实现细节的方向是否正确?
这个方向完全正确,是可维护测试的核心原则。不过你的测试代码有个小问题:
- 当前测试断言的是文件大小,但
parse方法本身并不负责写入文件——Scrapy的管道(Pipeline)才是处理数据持久化的组件。你的测试混淆了parse的职责(生成Request和Item)和数据写入的职责。 - 优化建议:测试
parse方法时,直接断言它生成的Request数量、Item的字段是否正确,而不是检查文件。文件写入的测试应该单独针对Pipeline组件。
额外测试建议
- 测试分页逻辑:单独构造包含“下一页”按钮和不包含的响应,测试
parse方法是否正确生成下一页的Request,或者在到达页数限制时停止。 - 测试错误场景:构造404、500的响应,测试爬虫是否能处理(比如跳过错误页面、重试)。
- 使用Scrapy的测试工具:Scrapy自带
scrapy test命令和TestCase类,可以更方便地模拟爬虫运行,比手动构造HtmlResponse更贴合真实场景。 - 隔离测试环境:测试时不要用生产环境的文件路径,而是用临时文件(比如
tempfile模块),避免测试污染生产数据,也让测试更可靠。
内容的提问来源于stack exchange,提问作者somedude
相关产品推荐
相关产品推荐

