You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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文件来模拟请求,避免每次测试都发真实请求,觉得这属于中等规模测试(不发真实请求但访问文件系统)。现在有几个疑惑:

  1. 这个测试是中等范围的集成测试还是窄范围的单元测试?它测试单个方法,但依赖Scrapy内置方法,让我不确定。
  2. 延伸问题:项目大部分基于Scrapy,自定义方法都会用到Scrapy或其他库的方法,我不清楚怎么定义单元测试,好像大部分测试都是集成或端到端测试,这是不是说明我的代码结构有问题?
  3. 项目要向多个博物馆发大量请求并存储数据,用录制请求存JSON的方式合适吗?有没有更好的方法测试这类操作并使用真实模拟数据?
  4. 我想让测试可维护,尽量清晰简洁、关注行为而非实现细节,不知道方向对不对。

希望得到针对这类项目测试的建议,谢谢!


解答

针对你的问题和代码,逐一拆解解答:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 21:54:51