全面掌握 Jest:从零开始的测试指南(下篇)
在上一篇测试指南中,我们介绍了Jest 的背景、如何初始化项目、常用的匹配器语法以及钩子函数的使用。这一篇篇将继续深入探讨 Jest 的高级特性,包括 Mock 函数、异步请求的处理、Mock 请求的模拟、类的模拟以及定时器的模拟、snapshot 的使用。通过这些技术,我们将能够更高效地编写和维护测试用例,尤其是在处理复杂异步逻辑和外部依赖时。
Mock 函数
假设存在一个 runCallBack
函数,其作用是判断入参是否为函数,如果是,则执行传入的函数。
export const runCallBack = (callback) => {typeof callback == "function" && callback();
};
编写测试用例
我们先尝试编写它的测试用例:
import { runCallBack } from './func';
test("测试 runCallBack", () => {const fn = () => {return "hello";};expect(runCallBack(fn)).toBe("hello");
});
此时,命令行会报错提示 runCallBack(fn)
执行的返回值为 undefined
,而不是 "hello"
。如果期望得到正确的返回值,就需要修改原始的 runCallBack
函数,但这种做法不符合我们的测试预期——我们不希望为了测试而改变原有的业务功能。
这时,mock
函数就可以很好地解决这个问题。mock 可以用来模拟一个函数,并可以自定义函数的返回值。我们可以通过 mock 函数来分析其调用次数、入参和出参等信息。
使用 mock 解决问题
上述测试用例可以改为如下形式:
test("测试 runCallBack", () => {const fn = jest.fn();runCallBack(fn);expect(fn).toBeCalled();expect(fn.mock.calls.length).toBe(1);
});
这里,toBeCalled()
用于检查函数是否被调用过,fn.mock.calls.length
用于检查函数被调用的次数。
mock 属性中还有一些有用的参数:
-
calls: 数组,保存着每次调用时的入参。
-
instances: 数组,保存着每次调用时的实例对象。
-
invocationCallOrder: 数组,保存着每次调用的顺序。
-
results: 数组,保存着每次调用的执行结果。
自定义返回值
mock
还可以自定义返回值。可以在 jest.fn
中定义回调函数,或者通过 mockReturnValue
、mockReturnValueOnce
方法定义返回值。
test("测试 runCallBack 返回值", () => {const fn = jest.fn(() => {return "hello";});createObject(fn);expect(fn.mock.results[0].value).toBe("hello");fn.mockReturnValue('alice') // 定义返回值createObject(fn);expect(fn.mock.results[1].value).toBe("alice");fn.mockReturnValueOnce('x') // 定义只返回一次的返回值createObject(fn);expect(fn.mock.results[2].value).toBe("x");createObject(fn);expect(fn.mock.results[3].value).toBe("alice");
});
构造函数的模拟
构造函数作为一种特殊的函数,也可以通过 mock
实现模拟。
// func.js
export const createObject = (constructFn) => {typeof constructFn == "function" && new constructFn();
};// func.test.js
import { createObject } from './func';
test("测试 createObject", () => {const fn = jest.fn();createObject(fn);expect(fn).toBeCalled();expect(fn.mock.calls.length).toBe(1);
});
通过使用 mock
函数,我们可以更好地模拟函数的行为,并分析其调用情况。这样不仅可以避免修改原有业务逻辑,还能确保测试的准确性和可靠性。
异步代码
在处理异步请求时,我们期望 Jest 能够等待异步请求结束后再对结果进行校验。测试请求接口地址使用http://httpbin.org/get
,可以将参数通过 query 的形式拼接在 URL 上,如 http://httpbin.org/get?name=alice
。这样接口返回的数据中将携带 { name: 'alice' }
,可以依此来对代码进行校验。
以下分别通过异步请求回调函数、Promise 链式调用、await 的方式获取响应结果来进行分析。
回调函数类型
回调函数的形式通过 done()
函数告诉 Jest 异步测试已经完成。
在 func.js
文件中通过 Axios
发送 GET
请求:
const axios = require("axios");export const getDataCallback = (url, callbackFn) => {axios.get(url).then((res) => {callbackFn && callbackFn(res.data);},(error) => {callbackFn && callbackFn(error);});
};
在func.test.js
文件中引入发送请求的方法:
import { getDataCallback } from "./func";
test("回调函数类型-成功", (done) => {getDataCallback("http://httpbin.org/get?name=alice", (data) => {expect(data.args).toEqual({ name: "alice" });done();});
});test("回调函数类型-失败", (done) => {getDataCallback("http://httpbin.org/xxxx", (data) => {expect(data.message).toContain("404");done();});
});
promise类型
在 Promise
类型的用例中,需要使用 return
关键字来告诉 Jest
测试用例的结束时间。
// func.js
export const getDataPromise = (url) => {return axios.get(url);
};
Promise
类型的函数可以通过 then 函数来处理:
// func.test.js
test("Promise 类型-成功", () => {return getDataPromise("http://httpbin.org/get?name=alice").then((res) => {expect(res.data.args).toEqual({ name: "alice" });});
});test("Promise 类型-失败", () => {return getDataPromise("http://httpbin.org/xxxx").catch((res) => {expect(res.response.status).toBe(404);});
});
也可以直接通过 resolves
和 rejects
获取响应的所有参数并进行匹配:
test("Promise 类型-成功匹配对象t", () => {return expect(getDataPromise("http://httpbin.org/get?name=alice")).resolves.toMatchObject({status: 200,});
});test("Promise 类型-失败抛出异常", () => {return expect(getDataPromise("http://httpbin.org/xxxx")).rejects.toThrow();
});
await 类型
上述 getDataPromise
也可以通过 await 的形式来编写测试用例:
test("await 类型-成功", async () => {const res = await getDataPromise("http://httpbin.org/get?name=alice");expect(res.data.args).toEqual({ name: "alice" });
});test("await 类型-失败", async () => {try {await getDataPromise("http://httpbin.org/xxxx")} catch(e){expect(e.status).toBe(404)}
});
通过上述几种方式,可以有效地编写异步函数的测试用例。回调函数
、Promise 链式调用
以及 await
的方式各有优劣,可以根据具体情况选择合适的方法。
Mock 请求/类/Timers
在前面处理异步代码时,是根据真实的接口内容来进行校验的。然而,这种方式并不总是最佳选择。一方面,每个校验都需要发送网络请求获取真实数据,这会导致测试用例执行时间较长;另一方面,接口格式是否满足要求是后端开发者需要着重测试的内容,前端测试用例并不需要涵盖这部分内容。
在之前的函数测试中,我们使用了 Mock
来模拟函数。实际上,Mock
不仅可以用来模拟函数,还可以模拟网络请求和文件。
Mock 网络请求
Mock 网络请求有两种方式:一种是直接模拟发送请求的工具(如 Axios
),另一种是模拟引入的文件。
直接模拟 Axios
首先,在 request.js 中定义发送网络请求的逻辑:
import axios from "axios";export const fetchData = () => {return axios.get("/").then((res) => res.data);
};
然后,使用 jest
模拟 axios 即 jest.mock("axios")
,并通过 axios.get.mockResolvedValue
来定义响应成功的返回值:
const axios = require("axios");
import { fetchData } from "./request";jest.mock("axios");
test("测试 fetchData", () => {axios.get.mockResolvedValue({data: "hello",});return fetchData().then((data) => {expect(data).toEqual("hello");});
});
模拟引入的文件
如果希望模拟 request.js
文件,可以在当前目录下创建 __mocks__
文件夹,并在其中创建同名的 request.js
文件来定义模拟请求的内容:
// __mocks__/request.js
export const fetchData = () => {return new Promise((resolve, reject) => {resolve("world");});
};
使用 jest.mock('./request')
语法,Jest
在执行测试用例时会自动将真实的请求文件内容替换成 __mocks__/request.js
的文件内容:
// request.test.js
import { fetchData } from "./request";
jest.mock("./request");test("测试 fetchData", () => {return fetchData().then((data) => {expect(data).toEqual("world");});
});
如果部分内容需要从真实的文件中获取,可以通过 jest.requireActual()
函数来实现。取消模拟则可以使用 jest.unmock()
。
Mock 类
假设在业务场景中定义了一个工具类,类中有多个方法,我们需要对类中的方法进行测试。
// util.js
export default class Util {add(a, b) {return a + b;}create() {}
}// util.test.js
import Util from "./util";
test("测试add方法", () => {const util = new Util();expect(util.add(2, 5)).toEqual(7);
});
此时,另一个文件如 useUtil.js
也用到了 Util
类:
// useUtil.js
import Util from "./util";export function useUtil() {const util = new Util();util.add(2, 6);util.create();
}
在编写 useUtil
的测试用例时,我们只希望测试当前文件,并不希望重新测试 Util
类的功能。这时也可以通过 Mock
来实现。
在 __mock__
文件夹下创建模拟文件
可以在__mock__
文件夹下创建 util.js
文件,文件中定义模拟函数:
// __mock__/util.js
const Util = jest.fn()
Util.prototype.add = jest.fn()
Util.prototype.create = jest.fn();
export default Util;// useUtil.test.js
jest.mock("./util");
import Util from "./util";
import { useUtilFunc } from "./useUtil";test("useUtil", () => {useUtilFunc();expect(Util).toHaveBeenCalled();expect(Util.mock.instances[0].add).toHaveBeenCalled();expect(Util.mock.instances[0].create).toHaveBeenCalled();
});
在当前 .test.js
文件定义模拟函数
也可以在当前 .test.js
文件中定义模拟函数:
// useUtil.test.js
import { useUtilFunc } from "./useUtil";
import Util from "./util";
jest.mock("./util", () => {const Util = jest.fn();Util.prototype.add = jest.fn();Util.prototype.create = jest.fn();return Util
});
test("useUtil", () => {useUtilFunc();expect(Util).toHaveBeenCalled();expect(Util.mock.instances[0].add).toHaveBeenCalled();expect(Util.mock.instances[0].create).toHaveBeenCalled();
});
这两种方式都可以模拟类。
Timers
在定义一些功能函数时,比如防抖和节流,经常会使用 setTimeout
来推迟函数的执行。这类功能也可以通过 Mock
来模拟测试。
// timer.js
export const timer = (callback) => {setTimeout(() => {callback();}, 3000);
};
使用 done
异步执行
一种方式是使用 done
来异步执行:
import { timer } from './timer'test("timer", (done) => {timer(() => {done();expect(1).toBe(1);});
});
使用 Jest 的 timers 方法
另一种方式是使用 Jest
提供的 timers
方法,通过 useFakeTimers
启用假定时器模式,runAllTimers
来手动运行所有的定时器,并使用 toHaveBeenCalledTimes
来检查调用次数:
beforeEach(()=>{jest.useFakeTimers()
})test('timer测试', ()=>{const fn = jest.fn();timer(fn);jest.runAllTimers();expect(fn).toHaveBeenCalledTimes(1);
})
此外,还有 runOnlyPendingTimers
方法用来执行当前位于队列中的 timers,以及 advanceTimersByTime
方法用来快进 X 毫秒。
例如,在存在嵌套的定时器时,可以通过 advanceTimersByTime
快进来模拟:
// timer.js
export const timerTwice = (callback) => {setTimeout(() => {callback();setTimeout(() => {callback();}, 3000);}, 3000);
};// timer.test.js
import { timerTwice } from "./timer";
test("timerTwice 测试", () => {const fn = jest.fn();timerTwice(fn);jest.advanceTimersByTime(3000);expect(fn).toHaveBeenCalledTimes(1);jest.advanceTimersByTime(3000);expect(fn).toHaveBeenCalledTimes(2);
});
无论是模拟网络请求、类还是定时器,Mock
都是一个强大的工具,可以帮助我们构建可靠且高效的测试用例。
snapshot
假设当前存在一个配置,配置的内容可能会经常变更,如下所示:
export const generateConfig = () => {return {server: "http://localhost",port: 8001,domain: "localhost",};
};
toEqual 匹配
如果对它进行测试用例编写,最简单的方式就是使用 toEqual
匹配,如下所示:
import { generateConfig } from "./snapshot";test("测试 generateConfig", () => {expect(generateConfig()).toEqual({server: "http://localhost",port: 8001,domain: "localhost",});
});
但是这种方式存在一些问题:每当配置文件发生变更时,都需要修改测试用例。为了避免测试用例频繁修改,可以通过 snapshot 快照来解决这个问题。
toMatchSnapshot
通过 toMatchSnapshot
函数生成快照:
test("测试 generateConfig", () => {expect(generateConfig()).toMatchSnapshot();
});
第一次执行 toMatchSnapshot
时,会生成一个 __snapshots__
文件夹,里面存放着 xxx.test.js.snap 这样的文件,内容是当前配置的执行结果。
第二次执行时,会生成一个新的快照并与已有的快照进行比较。如果相同则测试通过;如果不相同,测试用例不通过,并且在命令行会提示你是否需要更新快照,如 “1 snapshot failed from 1 test suite. Inspect your code changes or press u to update them”。
按下 u 键之后,测试用例会通过,并且覆盖原有的快照。
快照的值不同
如果该函数每次的值不同,生成的快照也不相同,例如每次调用函数返回时间戳:
export const generateConfig = () => {return {server: "http://localhost",port: 8002,domain: "localhost",date: new Date()};
};
在这种情况下,toMatchSnapshot 可以接受一个对象作为参数,该对象用于描述快照中的某些字段应该如何匹配:
test("测试 generateConfig", () => {expect(generateConfig()).toMatchSnapshot({date: expect.any(Date)});
});
行内快照
上述的快照是在 __snapshots__
文件夹下生成的,还有一种方式是通过 toMatchInlineSnapshot
在当前的 .test.js 文件中生成。需要注意的是,这种方式通常需要配合 prettier
工具来使用。
test("测试 generateConfig", () => {expect(generateConfig()).toMatchInlineSnapshot({date: expect.any(Date),});
});
测试用例通过后,该用例的格式如下:
test("测试 generateConfig", () => {expect(generateConfig()).toMatchInlineSnapshot({date: expect.any(Date)
}, `
{"date": Any<Date>,"domain": "localhost","port": 8002,"server": "http://localhost",
}
`);
});
使用 snapshot
测试可以有效地减少频繁修改测试用例的工作量。无论配置如何变化,只需要更新一次快照即可保持测试的一致性。
文章转载自:一颗冰淇淋
原文链接:全面掌握 Jest:从零开始的测试指南(下篇) - 一颗冰淇淋 - 博客园
体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构
相关文章:

全面掌握 Jest:从零开始的测试指南(下篇)
在上一篇测试指南中,我们介绍了Jest 的背景、如何初始化项目、常用的匹配器语法以及钩子函数的使用。这一篇篇将继续深入探讨 Jest 的高级特性,包括 Mock 函数、异步请求的处理、Mock 请求的模拟、类的模拟以及定时器的模拟、snapshot 的使用。通过这些技…...

如何利用UML进行领域建模
如何利用UML进行领域建模 领域建模是通过创建抽象模型来反映业务领域中的核心概念、实体及其之间的关系。UML(统一建模语言)是进行领域建模的常用工具,它能帮助我们可视化地设计系统架构和业务逻辑。在这篇讲解中,我们将详细解释…...

Vue实用操作篇-1-第一个 Vue 程序
安装 Vue 非常的简便,只需下载好 Vue 对应的 .js 文件,在 html 中引入 vue.js 即可使用 Vue 下载好了 vue.js 我们便可以编写我们的第一个 vue 程序了 <!doctype html> <html lang"zh-CN"><head><meta charset"utf…...

Qwen2-VL的微调及量化
一、Qwen2-VL简介 Qwen2-VL是Qwen-VL的升级版本,能力更强,性能全面提升。尤其是72B参数的版本更是取了惊人的成绩。它可以读懂不同分辨率和不同长宽比的图片,在 MathVista、DocVQA、RealWorldQA、MTVQA 等基准测试创下全球领先的表现…...

[数据集][目标检测]车窗状态检测车窗开关检测数据集VOC+YOLO格式299张3类别
数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):299 标注数量(xml文件个数):299 标注数量(txt文件个数):299 标注类别…...

自动泊车系统中的YOLOv8 pose关键点车位线检测
自动泊车系统中的YOLOv8关键点车位线检测技术解析 引言 随着智能驾驶技术的快速发展,自动泊车功能成为了现代汽车的重要组成部分。它不仅能够提高驾驶的安全性,还能在一定程度上解决城市停车难的问题。在自动泊车系统中,准确识别停车位的位置…...

Java html生成pdf和图片
在 Java 项目中将 HTML 生成图片是一项常见需求,特别是用于生成报告、预览页面截图等。不同的库和工具在渲染能力、性能以及支持的功能上有所不同。以下是几种主流的技术选型和对比,帮助你选择适合的解决方案。 技术对比总结 技术名称优点缺点适用场景…...

JavaWeb笔记整理——Redis
目录 Redis数据类型 各种数据类型的特点 Redis常用命令 字符串操作命令 哈希操作命令 列表操作命令 集合操作命令 有序集合操作命令 通用命令 在Java中操作Redis Spring Data Redis的使用方式 操作字符串类型的数据 编辑操作hash类型的数据 编辑 操作列表类…...

数据库(mysql)常用命令
一.常见的数据库端口号 Mysql默认端口:3306 oracle 默认端口:1521 Sql server 默认端口:1433 注:Mysql采用 的是C/S(客户端/服务器端)架构 二.sql 语法基础 服务器,数据库,数据表,记录,字段之间的关系: 一台Mysql服务器可以管理多个数据库 一个数据库可以存在多张二维表…...

源网荷储一体化新型电力系统解决方案
风光装机快速增长,加剧电力系统不可控性。截至2023H1,我国风电装机389.21GW,太阳能装机470.67GW,风光合计占总装机的31.76%。其中,2023年H1我国风电新增装机22.99GW,对比22年同期新增12.94GW,同…...

树莓派安装 OpenCV 教程
以下是在树莓派上安装 OpenCV 的教程: 笔者当前Python版本:3.7.3 一、更新树莓派系统 在终端中运行以下命令: sudo apt update sudo apt upgrade二、安装必要的依赖项 安装构建工具和图像 I/O 库: sudo apt install build-e…...

01,大数据总结,zookeeper
1 ,zookeeper :概述 1.1,zookeeper:作用 1 ,大数据领域 :存储配置数据 例如:hadoop 的 ha 配置信息,hbase 的配置信息,都存储在 zookeeper 2 ,应用领…...

伪工厂模式制造敌人
实现效果 1.敌人方实现 敌人代码 using UnityEngine; using UnityEngine.UI;public class EnemyBasics : MonoBehaviour {public int EnemySpeed { get; internal set; }public int EnemyAttackDistance { get; internal set; }public int EnemyChaseDistance { get; interna…...

【linux】pwd命令
pwd 命令在 Linux 和类 Unix 系统中用于显示当前工作目录的完整路径。它是 "print working directory" 的缩写。 当你在终端或命令行界面中工作时,你可能会在不同的目录(或文件夹)之间切换。pwd 命令帮助你确定你当前位于哪个目录…...

Python 如何封装工具类方法,以及使用md5加密
第一步:封装使用方法 在utils目录中,编写我的md5加密的方法,如下: import re import hashlib from os import path from typing import Callable from flask import current_app# 这里封装的是工具类的方法def basename(filenam…...

网络编程的应用
目录 1.单机程序和网络程序 2.客户端与服务端 3.网络编程三要素 3.1 IP地址 3.2 port端口 4.TCP编程 5.UDP编程 1.单机程序和网络程序 之前编写的程序都是单机程序,所有的业务功能实现及数据存储都在一个主机上完成,我们称为单机程序 我们在生活…...

佰朔资本:国内海风加速招标 船舶行业景气上行
昨日,沪指盘中一度下探失守2700点,尾盘在地产、银行等板块的带动下发力上扬,深证成指亦翻红。到收盘,沪指涨0.49%报2717.28点,深证成指涨0.11%报7992.25点,创业板指跌0.11%报1533.47点,上证50指…...

理解AAC和Opus的编码与解码流程
理解AAC和Opus的编码与解码流程及其在Android中的实现,对于音频开发非常重要。下面,我将详细解释这两种编码格式的原理、流程,并结合具体代码示例,帮助你在Android项目中合理地设计和使用它们。 一、AAC(Advanced Audio Coding) 1. AAC的原理与流程 AAC是一种有损音频压…...

设计图纸加密方法知多少?小编给你讲清楚
一、对称加密 使用对称加密算法,对设计图纸进行加密。对称加密使用相同的密钥进行加密和解密,确保只有持有正确密钥的人能够解密文件。 二、非对称加密 使用非对称加密算法,进行设计图纸的加密。非对称加密使用公钥加密、私钥解密的方式&a…...

pycv实时目标检测快速实现
使用python_cv实现目标实时检测 python 安装依赖核心代码快速使用实现结果展示enjoy python 安装依赖 opencv_python4.7.0.72 pandas1.5.3 tensorflow2.11.0 tensorflow_hub0.13.0 tensorflow_intel2.11.0 numpy1.23.5核心代码快速使用 # 使用了TensorFlow Hub和OpenCV库来实…...

记录下如何让字体在div内 自动换行 上下居中
div内样式 display: flex; // flex布局 justify-content: center; // 上下居中 align-items: center; // 左右居中 overflow-wrap: break-word; // 允许字体换行 (若行内的单词无法放下则换行) word-break: break-all; // 强制文本在任意字符间进…...

Shell篇之编写MySQL启动脚本
Shell篇之编写MySQL启动脚本 1. 脚本内容 vim mysql_ctl.sh#!/bin/bashmysql_port3306 mysql_username"root" mysql_password"molinker" mysql_conf"/opt/lanmp/mysql/etc/my.cnf" mysql_sock"/opt/lanmp/mysql/var/mysql.sock"func…...

supermap Iclient3d for cesium加载地形并夸大地形
先看效果图 这是没有夸张之前的都江堰 这是夸大五倍后的都江堰 下面展示代码 主要就是加载supermaponline的skt地形然后夸大 <template><div class"PartOneBox"><div id"cesiumContainer"></div></div> </template>…...

一文解读OLAP的工具和应用软件
OLAP(OnlineAnalyticalProcessing)是一种用于快速分析大规模、多维度数据的方法。OLAP工具和应用软件则是帮助人们进行OLAP分析的重要工具。本文将介绍几种常见的OLAP工具和应用软件,并探讨它们在数据分析中的作用。 一 OLAP工具的分类 在选…...

xml重点笔记(尚学堂 3h)
XML:可扩展标记语言 主要内容(了解即可) 1.XML介绍 2.DTD 3.XSD 4.DOM解析 6.SAX解析 学习目标 一. XML介绍 1.简介 XML(Extensible Markup Language) 可扩展标记语言,严格区分大小写 2.XML和HTML XML是用来传输和存储数据的。 XML多用在框架的配置文件…...

爬虫代理API的全面解析:让数据抓取更高效
在大数据时代,网络爬虫已经成为收集和分析数据的重要工具。然而,频繁的请求会导致IP被封禁,这时候爬虫代理API就显得尤为重要。本文将详细介绍爬虫代理API的作用、优势及如何使用,帮助你更高效地进行数据抓取。 什么是爬虫代理AP…...

PCL 点云中的植被信息提取(C++详细过程版)
目录 一、算法原理1、原理概述2、参考文献二、代码实现三、结果展示本文由CSDN点云侠原创,原文链接,首发于:2024年9月18日。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的抄袭狗。 一、算法原理 1、原理概述 点云具有丰富的色彩信息,可以与植被指数结合使…...

requests-html的具体使用方法有哪些?
requests-html是一个功能强大的Python库,用于发送HTTP请求和解析HTML内容。它的使用方法包括安装库、基本使用、发送带有参数的请求、图片抓取实战案例、解析网页内容、执行JavaScript代码、使用CSS选择器来查找元素、继续跟踪链接并获取内容等。 安装request…...

YOLOv9改进策略【卷积层】| AKConv: 具有任意采样形状和任意参数数量的卷积核
一、本文介绍 本文记录的是利用AKConv优化YOLOv9的目标检测网络模型。标准卷积操作的卷积运算局限于局部窗口,无法捕获其他位置的信息,且采样形状固定,无法适应不同数据集和位置中目标形状的变化。而AKConv旨在为卷积核提供任意数量的参数和…...

图神经网络池化方法
图神经网络池化方法 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 图神经网络池化方法前言一、扁平图池化二、分层图池化1.节点聚类池化2.节点丢弃池化 参考文献 前言 图池化操作根据其池化策略的差异ÿ…...