GeoAI CLI 测试体系深度解析:60 个单元与端到端测试如何守护 8 大核心模块

原创2026-10-03 19:52:57197 阅读
文章标签:人工智能深度学习计算机视觉数据可视化数据分析

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.py 42 个、test_full_e2e.py 18 个(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 用例的测试体系,其设计有四点值得复用:

  1. 分层验证:单元层直接测 core 函数(快速、精确),E2E 层测完整命令行链路(真实、可信),两层互补;
  2. 合成数据优先:用 np.random + rasterio 现场生成测试栅格,使测试零外部依赖、完全可复现;
  3. 错误路径优先于重模型:对需要 GPU/模型的推理功能,先把文件校验、异常映射(FileNotFoundError/ValueError/IndexError → JSON 错误响应)测扎实;
  4. 面向 Agent 的断言:所有子进程测试都解析 --json 输出并断言结构化字段,确保 CLI 的机器可读契约长期稳定。

这套测试不仅守护了 cli-anything-geoai 的 8 大核心模块,也为"CLI 工具如何为 AI Agent 提供可靠接口"提供了可参照的工程样板。

登录后查看全文
geoai