GeoAI CLI 测试体系深度解析:60 个单元与端到端测试如何守护 8 大核心模块
GeoAI CLI 测试体系深度解析:60 个单元与端到端测试如何守护 8 大核心模块
本篇技术指南围绕 GeoAI 命令行工具 cli-anything-geoai 的完整测试计划与测试结果展开,详细剖析 测试计划文档 中规划的 60 个测试用例,结合 test_core.py 与 test_full_e2e.py 的源码实现,逐模块拆解单元测试设计思路、端到端工作流验证方式以及最终的 100% 通过率运行结果。读完本文,你将掌握该 CLI 测试体系的整体架构、每个测试的验证重点,以及如何在自己环境中复现这套测试。
测试体系总览:两级测试矩阵,覆盖完整命令行能力
GeoAI CLI 的测试体系采用单元测试 + 端到端测试两级分层策略,全部集中在 agent-harness/cli_anything/geoai/tests/ 目录下。测试计划文档中的清单如下:
| 文件 | 类型 | 计划测试数 |
|---|---|---|
| test_core.py | 单元测试 | ~40 tests |
| test_full_e2e.py | E2E + 子进程测试 | ~20 tests |
计划总计:约 60 个测试用例。
从源码结构看,这一设计遵循了清晰的分层原则:单元测试直接调用 cli_anything.geoai.core 下的各核心模块函数(project、session、raster、vector、data、segment、detect、change 共 8 个模块),用合成数据验证逻辑正确性;E2E 测试则使用真实 GeoTIFF 数据,甚至通过 subprocess 调用已安装的 cli-anything-geoai 命令,验证从命令行入口到最终输出的完整链路,且全程无 mock(见 test_full_e2e.py 文件头注释)。
单元测试设计(test_core.py):42 个用例逐模块把关
单元测试模块 test_core.py 的核心思路是"能用合成数据就不用真实模型":文件头注释明确写道"使用合成数据测试核心逻辑,真实 GeoTIFF 用于栅格/矢量操作,这里不做模型推理测试"(test_core.py)。
测试夹具:合成 GeoTIFF 与 GeoJSON
为保证测试可复现且不依赖外部数据,该文件提供了两个关键 fixture:
sample_raster:用rasterio现场生成一个 3 波段 64×64 的float32合成 GeoTIFF,坐标为EPSG:4326,范围约在 Knoxville 地区(-84.0, 35.9, -83.9, 36.0),数据为np.random.rand随机值(test_core.py)。随机值保证了 min/max/mean 断言天然落在[0,1)区间内。sample_vector:用geopandas构造一个包含 3 个box面要素的 GeoJSON(test_core.py)。
core/project.py(10 个测试):项目管理全生命周期
项目模块对应 core/project.py,测试覆盖了项目对象从创建到销毁的完整生命周期:
test_create_project_defaults:验证默认项目名为"untitled"、默认 CRS 为"EPSG:4326"、files与results均为空列表、且含created时间戳——这与create_project()的实现字段完全对应(project.py)。test_create_project_custom:验证自定义名称与 CRS(如EPSG:32617)生效。test_open_project_valid/test_open_project_missing_file/test_open_project_invalid_json:分别验证正常读取、FileNotFoundError(文件不存在)与ValueError(非法 JSON)三种路径。其中非法 JSON 的错误消息匹配"Invalid JSON",与 open_project() 的raise ValueError(f"Invalid JSON in project file: {e}")一一对应。test_save_project:保存后重新读取,验证 round-trip 往返一致性。test_get_project_info:构造 2 个 raster + 1 个 vector 的文件列表,验证file_count == 3、raster_count == 2、vector_count == 1,对应get_project_info()的计数逻辑(project.py)。test_add_file_raster/test_add_file_auto_detect:验证显式指定类型与按扩展名自动检测(.tif→raster)两种添加方式,自动检测逻辑实现在_detect_file_type()中,支持 raster/vector/model/config 四类扩展名(project.py)。test_remove_file:按索引移除文件,并验证越界索引抛出IndexError,与remove_file()的边界检查一致(project.py)。
core/session.py(10 个测试):快照式撤销/重做机制
会话模块对应 core/session.py,其核心是基于深拷贝快照的 undo/redo 栈:
test_session_empty/test_session_set_project/test_session_get_project_no_project:验证会话初始无项目、加载项目后has_project()为真、未加载时get_project()抛出RuntimeError(消息匹配"No project loaded")。test_session_snapshot_and_undo/test_session_redo:验证"快照 → 修改 → 撤销恢复原状 → 重做恢复修改"的完整往返,且undo()返回快照描述"rename"。test_session_undo_empty/test_session_redo_empty:空栈时分别抛出RuntimeError(消息"Nothing to undo"/"Nothing to redo")。test_session_history:连续 3 次快照后history()返回 3 条记录,且描述与时间戳字段完整。test_session_max_undo_limit:将MAX_UNDO临时设为 5 后连做 10 次快照,断言_undo_stack长度被裁剪为 5 且保留的是最新 5 条(栈首为"step 5")。这直接验证了snapshot()中if len(self._undo_stack) > self.MAX_UNDO: self._undo_stack.pop(0)的裁剪逻辑(session.py)。默认MAX_UNDO = 50。test_session_save_and_load:save_session()写出文件后可被open_project()重新载入,验证会话持久化能力。注意save_session()使用"临时文件 +os.replace"的原子写入策略(session.py)。
core/raster.py(6 个测试):栅格读写与处理
栅格模块对应 core/raster.py,测试验证:
test_get_raster_info:返回 dict 且含crs或driver键。实现上优先调用geoai.utils.get_raster_info,失败时回退到 rasterio 直接读取(返回 driver/dtype/width/height/bands/crs/bounds/resolution/nodata/file_size_bytes 等字段,见 raster.py)。test_get_raster_stats:验证 band 号、min/max/mean/std、valid_pixels > 0,并断言统计值落在<a href="https://link.gitcode.com/i/85227bf302d1adfb2e9163bd9b4d497a" target="_blank">0, 1)(因为合成数据范围如此)。实现中通过 nodata 掩码与np.isnan掩码剔除了无效像素([raster.py)。test_get_raster_stats_bad_band:band 号越界(10)时抛出ValueError,消息匹配"Band .* out of range"。test_tile_raster:以tile_size=32, overlap=0切分栅格,验证tile_count > 0且输出目录存在。实现中stride = tile_size - overlap,并递归统计.tif/.tiff文件(raster.py)。test_vectorize_raster:构造一个 3×3 的分类栅格(值为 1/2/3),矢量化后验证 GeoJSON 输出存在且feature_count > 0。
core/vector.py(4 个测试):矢量元数据与栅格化
矢量模块对应 core/vector.py:
test_get_vector_info:验证合成 GeoJSON 的feature_count == 3。test_get_vector_info_missing_file:不存在文件抛FileNotFoundError。test_rasterize_vector:以合成栅格为模板栅格化矢量(烧录value属性),验证输出文件存在且宽高大于 0。核心是vector_to_raster(path, output, reference_raster=template, attribute_field=attribute)(vector.py)。test_rasterize_vector_missing_file:缺失矢量文件抛FileNotFoundError。
core/data.py(4 个测试):数据发现与边界框解析
数据模块对应 core/data.py:
test_parse_bbox_valid:"-84.0,35.9,-83.9,36.0"被正确解析为 4 元浮点元组。test_parse_bbox_invalid:"not,a,bbox"与"1.0,2.0"均抛出ValueError(消息匹配"Invalid bbox"),对应parse_bbox()中"必须恰好 4 个浮点值"的校验(data.py)。test_list_sources:数据源列表非空,且每项含name与description字段。test_list_sources_contains_naip:确认naip在数据源列表中——DATA_SOURCES = <a href="https://link.gitcode.com/i/e49d86c954b51cc284293cb65b86a0d1" target="_blank">"naip", "overture", "stac"]([data.py)。
core/segment.py / detect.py / change.py(共 8 个测试):AI 能力的"错误路径"守护
这三个模块对应 segment.py、detect.py、change.py,测试的共同特点是只测能力枚举与错误处理,不跑真实模型推理:
test_list_sam_models:SAM 模型列表非空且含model_id,并确认"sam-vit-huge"在列。SAM_MODELS常量列出了从facebook/sam-vit-huge到facebook/sam2.1-hiera-tiny的 7 个模型(segment.py)。test_list_architectures:架构列表包含unet与deeplabv3,SEGMENTATION_ARCHITECTURES共列出 9 种架构(segment.py)。test_run_sam_missing_file:输入栅格缺失时抛FileNotFoundError——run_sam()在真正调用SamGeo之前先做文件存在性检查(segment.py)。test_list_models:检测模型列表包含maskrcnn_resnet50_fpn,DETECTION_MODELS共 7 个模型(detect.py)。test_list_input_formats:输入格式列表包含coco与yolo,INPUT_FORMATS = <a href="https://link.gitcode.com/i/94cfd6e04ce5c18cdf102d385fe196e2" target="_blank">"directory", "coco", "yolo"]([detect.py)。test_run_detection_missing_file:栅格或模型缺失时抛FileNotFoundError。test_list_methods:变化检测方法列表包含changestar与anychange。test_detect_changes_missing_file:两张输入影像缺失时抛FileNotFoundError。
这些测试与 CLI 的错误处理装饰器 handle_error(geoai_cli.py)配合:该装饰器捕获 ValueError、IndexError、FileNotFoundError、RuntimeError、OSError,在 JSON 模式下输出 {"error": ..., "type": ...},在非 REPL 模式下以退出码 1 结束——这正是单元测试中大量 pytest.raises 断言能够在命令行层面被正确转换的前提。
E2E 测试设计(test_full_e2e.py):真实数据 + 真实进程 + 真实工作流
E2E 测试模块 test_full_e2e.py 在单元测试之上叠加了三层强度:真实 GeoTIFF 数据、已安装命令的子进程调用、多步骤工作流模拟。文件头明确声明"使用真实 GeoTIFF 文件与已安装的 cli-anything-geoai 命令通过 subprocess 测试完整 CLI 管线,无 mock"(test_full_e2e.py)。
Raster Operations E2E(4 个测试)
test_raster_info_real_file:对真实 Knoxville Landsat GeoTIFF(knoxville_landsat_2022.tif,位于仓库根目录)读取元数据,验证返回 dict。该测试用@pytest.mark.skipif保护:文件缺失时自动跳过而非失败(test_full_e2e.py)。test_raster_stats_all_bands:遍历真实 Landsat 影像的所有波段计算统计量,验证每个波段的band号、valid_pixels > 0,并检查 min/max 为浮点数。test_raster_tile_and_count:用tile_size=64, overlap=0切分合成栅格,验证tile_count > 0且输出目录存在。test_raster_vectorize_real:构造 4×4 分类栅格(四个 2×2 区块),矢量化后验证 GeoJSON 输出存在、feature_count > 0且文件非空。
Project Workflow E2E(3 个测试)
test_project_create_add_save_reload:完整生命周期——创建e2e_test项目 → 添加栅格文件(自动检测类型)→ 保存 JSON → 重新载入,逐项断言名称、文件数与文件类型。test_project_session_undo_redo:在会话层面对"添加文件"操作做快照 → 撤销(文件数回到 0)→ 重做(文件数回到 1),验证 undo/redo 在真实项目操作中的可用性。test_project_info_after_multiple_files:添加两个栅格后file_count == 2且raster_count == 2。
CLI Subprocess Tests(8 个测试):命令行级验证
这是整个测试体系中与"CLI 形态"绑定最紧密的一组。核心辅助函数 _resolve_cli() 负责解析命令入口:优先使用 PATH 中已安装的 cli-anything-geoai;若未安装则回退到 python -m cli_anything.geoai.geoai_cli(test_full_e2e.py)。同时支持 CLI_ANYTHING_FORCE_INSTALLED=1 环境变量强制要求已安装命令,否则抛错提示 pip install -e .。
8 个用例覆盖:
test_cli_help:--help退出码为 0 且输出含"GeoAI CLI"(与 geoai_cli.py 的 docstring 一致)。test_cli_version:--version输出含"1.0.0"(对应click.version_option(version="1.0.0"),geoai_cli.py)。test_cli_raster_info_json:--json raster info <file>输出可被json.loads解析的 dict。test_cli_raster_stats_json:--json raster stats <file>输出band == 1且min为 float。test_cli_project_new_json:--json project new -n subprocess_test -o <file>创建项目文件,输出 dict 中name正确。test_cli_system_info_json:--json system-info输出含geoai_version与torch_version键。test_cli_session_status_json:--json session status输出含has_project键。注意此用例传check=False——未加载项目时退出码为 1(错误路径),但 JSON 输出依然有效,这验证了错误响应也是结构化 JSON 的设计(geoai_cli.py)。test_cli_segment_list_models:--json segment list-models输出非空 JSON 列表且首项含model_id。
Realistic Workflow Scenarios(3 个测试):模拟 Agent 真实使用
这三组场景以"模拟 Agent 连续调用 CLI 完成任务"为设计目标,每个场景由多个子进程命令串联而成:
- Raster Analysis Pipeline(
test_raster_analysis_pipeline):模拟分析卫星影像元数据与统计量的场景,操作链为raster info → raster stats (--band 1) → project new,验证所有 JSON 响应有效且统计量为数值。 - Project Management Workflow(
test_project_management_workflow):模拟搭建地理空间 AI 工作区,操作链为project new → 校验项目文件 JSON → 文件列表 → 保存 → 重开,验证文件数与重载正确性。 - Data Discovery Workflow(
test_data_discovery_workflow):模拟探索可用数据源与模型,操作链为data sources → segment list-models → detect list-models → change list-methods,验证所有 list 命令返回非空结果。
这三个场景直接呼应了 CLI 面向 AI Agent 的设计定位——--json 机器可读输出让 Agent 可以"先检查、再处理、后验证"(参见 README.md 中"Using with Claude Code"章节的用法示例)。
测试结果与覆盖分析:60/60 通过,9.90 秒完成
测试计划文档记录了一次完整的执行结果(运行日期 2026-04-11,环境为 Python 3.12.12、pytest 9.0.2、Linux 平台,rootdir 为 agent-harness,使用 pytest.ini 配置):
- 收集 60 个测试项,其中
test_core.py42 个、test_full_e2e.py18 个(E2E 栅格 4 + 项目工作流 3 + CLI 子进程 8 + 真实工作流 3); - 全部通过:
60 passed in 9.90s,通过率 100%; - 运行输出按测试类分组展示:
TestProject、TestSession、TestRaster、TestVector、TestData、TestSegment、TestDetect、TestChange、TestRasterE2E、TestProjectWorkflowE2E、TestCLISubprocess、TestRealisticWorkflows。
结果汇总表:
| 指标 | 数值 |
|---|---|
| 测试总数 | 60 |
| 通过 | 60 |
| 失败 | 0 |
| 通过率 | 100% |
| 耗时 | 9.90s |
覆盖说明要点
测试计划文档给出了四层覆盖维度的明确标注:
- 单元测试:42 个测试覆盖全部 8 个 core 模块;
- E2E 栅格测试:4 个测试使用真实 GeoTIFF 数据(Knoxville Landsat + 合成数据);
- E2E 项目工作流测试:3 个测试覆盖创建/保存/加载完整生命周期;
- CLI 子进程测试:8 个测试经由已安装的
cli-anything-geoai命令执行; - 工作流集成测试:3 个多步骤场景模拟真实 Agent 使用。
明确的测试边界:GPU/模型类功能仅测错误路径
文档特别披露了刻意保留的测试盲区:
未测试(需 GPU/模型):SAM 推理、模型训练、变化检测推理。这些功能需要下载模型并具备 GPU 环境。CLI 中对应命令的错误处理(缺失文件)已被测试,但未做端到端推理验证。
这解释了为何 test_core.py 中 segment/detect/change 三个模块只安排了"枚举列表 + 缺失文件异常"类测试——重模型推理依赖模型下载与 GPU 资源,不适合放入必须快速、稳定通过的常规测试套件;取而代之的是通过 FileNotFoundError 前置校验确保错误路径可控,这从 segment.py 等实现中可以看到:所有推理函数都在加载模型前先做 os.path.isfile 检查。
在本仓库环境中复现测试
测试运行方式记录在 README.md 中。仓库根目录下即可执行:
# 单元 + E2E 全量测试
cd agent-harness
python -m pytest cli_anything/geoai/tests/ -v -s
强制使用已安装命令而非 python -m 回退(需要先 pip install -e .):
CLI_ANYTHING_FORCE_INSTALLED=1 python -m pytest cli_anything/geoai/tests/ -v -s
几点前提说明:
test_full_e2e.py中涉及真实 Landsat 数据的用例带有@pytest.mark.skipif(not os.path.isfile(LANDSAT_2022), ...)保护,如果仓库根目录缺少knoxville_landsat_2022.tif,这些用例会自动跳过而非失败;- E2E 子进程用例在
cli-anything-geoai未安装时会自动回退到python -m cli_anything.geoai.geoai_cli,两种方式均被支持; - 8 个 core 模块的依赖(
rasterio、geopandas、numpy、click等)需预先安装,且cli_anything.geoai.core中的各函数在geoai主库未安装时会使用 rasterio/geopandas 的本地回退实现(如 raster.py 的try/except ImportError分支),这保证了测试在最小依赖环境下也能运行。
结语:测试设计对 CLI 工具的启示
回顾这套 60 用例的测试体系,其设计有四点值得复用:
- 分层验证:单元层直接测 core 函数(快速、精确),E2E 层测完整命令行链路(真实、可信),两层互补;
- 合成数据优先:用
np.random+rasterio现场生成测试栅格,使测试零外部依赖、完全可复现; - 错误路径优先于重模型:对需要 GPU/模型的推理功能,先把文件校验、异常映射(
FileNotFoundError/ValueError/IndexError→ JSON 错误响应)测扎实; - 面向 Agent 的断言:所有子进程测试都解析
--json输出并断言结构化字段,确保 CLI 的机器可读契约长期稳定。
这套测试不仅守护了 cli-anything-geoai 的 8 大核心模块,也为"CLI 工具如何为 AI Agent 提供可靠接口"提供了可参照的工程样板。