Files
osmWorkflow/.trellis/spec/blender/testing.md

7.1 KiB
Raw Blame History

测试

适用:改动 osmassets/osm.pyosmassets/geom.py,或往里加新的纯函数。


怎么跑

python3 -m unittest discover blender/tests

不需要 Blender,用系统 Python 就行。当前 51 个用例,运行约 0.01 秒。

这是全仓唯一的自动化测试。快到没有理由不在每次改动后跑一遍。

能这么跑的前提是 osmassets依赖分层—— test_pure.py:19 手动把 blender/ 塞进 sys.path,然后只 import 纯 Python 模块。


最重要的一条:期望值必须从几何推导

test_pure.py:9-11 写得很直白:

The expected values are derived from the geometry, not captured from the implementation — a test that just records current output would ratify a bug.

具体做法是在测试里写清楚为什么是这个数:

def test_half_outside_polygon_is_cut_at_the_boundary(self):
    clipped = clip_polygon(SQUARE, 0.0, 5.0, 0.0, 10.0)
    self.assertTrue(all(x <= 5.0 + 1e-9 for x, _ in clipped))
    # A 10x10 square clipped to half its width is a 5x10 rectangle.
    self.assertAlmostEqual(polygon_area(clipped), 50.0, places=6)

反例——不要这样写

def test_clip(self):
    # 跑一遍把输出粘过来
    self.assertEqual(clip_polygon(SQUARE, 0.0, 5.0, 0.0, 10.0),
                     [(0.0, 0.0), (5.0, 0.0), (5.0, 10.0), (0.0, 10.0)])

这种测试在实现正确时和实现错误时同样会通过。它固化的是当前行为,不是需求。

自检:把被测的那个特性从实现里删掉,测试还能过吗?能过就是无效测试。


每个几何函数都要覆盖退化输入

这是本套测试最系统的部分。geom.py 的函数会收到真实 OSM 数据里的各种畸形几何, 所以每个函数都有一个 test_degenerate_input

退化情形 例子
空输入 clip_polygon([], ...)[] (:73)
点数不足成面 clip_polygon([(0,0),(1,1)], ...)[] (:74)
完全在裁剪框外 clip_polygon(SQUARE, 20,30,20,30)[] (:70)
零长线段 sample_tree_row 跳过而非除零 (:203)
重复顶点 distance_to_ring 不除零 (:178)
参数为零 sample_ring_boundary(SQUARE, spacing=0.0)[] (:123)
单点输入 sample_polygon_interior([(0,0)], ...)[] (:146)

加新几何函数就配一个 test_degenerate_input 约定是"返回空/零"而不是抛异常—— 一个坏多边形不该中断整片区域的构建。

除零守卫要指名道姓

覆盖某个具体守卫时,注释写清楚针对哪一行:

def test_axis_aligned_edge_does_not_divide_by_zero(self):
    # A vertical edge crossing the x clip plane exercises the b[0] == a[0]
    # guard in the intersection lambdas.

这样守卫被误删时,失败的测试能直接说明它保护的是什么(test_pure.py:76-78)。


其余几条约定

随机采样必须验 seed 可复现

def test_seed_is_deterministic(self):
    first  = sample_polygon_interior(SQUARE, spacing=3.0, seed=7)
    second = sample_polygon_interior(SQUARE, spacing=3.0, seed=7)
    self.assertEqual(first, second)

不可复现的采样会让 parity 校验永久性地红。 任何带随机的新函数都要收 seed 参数并配这个测试(test_pure.py:135-138)。

绕向无关的性质要两个方向都测

sample_ring_boundaryinset 对顺时针和逆时针都必须往内缩:

ccw = sample_ring_boundary(SQUARE, spacing=10.0, inset=1.0)
cw  = sample_ring_boundary(list(reversed(SQUARE)), spacing=10.0, inset=1.0)
self.assertTrue(all(point_in_polygon((x, y), SQUARE) for x, y, _, _ in ccw))
self.assertTrue(all(point_in_polygon((x, y), SQUARE) for x, y, _, _ in cw))

同理 polygon_area 对绕向不敏感、signed_polygon_area 敏感,两者分别验 test_pure.py:88-91, 109-115)。

跨边界的连续性要单独测

真实几何常见的坑是"分段处理时每段各自重新开始"

def test_spacing_carries_across_segment_joins(self):
    # Two 3m segments with 4m spacing: the second sample must land 1m into
    # the second segment, not restart at its origin.

test_pure.py:190-196

解析器的容错语义要写死

parse_osm 的两档行为必须都有测试(test_pure.py:343-356

  • 坏节点跳过,不致命lon='oops' 的节点被忽略,其余照常解析
  • bounds 直接抛 RuntimeError:没有 bounds 就无法建立投影,继续下去毫无意义

分档理由见 generate_scene.py:9-12OSM 导出可能带上区域外的 relation 成员, 所以范围只认 bounds 元素,不用全部节点算包围盒。

用真实格式的 fixture

OSM_SAMPLEtest_pure.py:285)是一段真的 OSM XML故意塞进了坏节点、 action='delete' 的 way、引用了不存在节点的 way。写进临时文件再解析 tearDownos.unlink

别 mock 解析器。 解析器的价值就在于处理真实世界的脏数据。


命名

  • 类名 = 被测函数的驼峰 + TestClipPolygonTestSampleTreeRowTest
  • 方法名是陈述句,说明这个行为是什么,不是 test_case_1 test_polygon_keeps_only_the_exterior_ringtest_trailing_point_is_skipped_when_it_would_double_plant

方法名读起来就是这个函数的规格说明。


测不到的部分怎么办

bpy 层(mesh.pymaterials.pytree.py、两个入口脚本)没有单元测试 也不打算有——它们需要真实的 Blender 运行时。

这一层的回归防线是 parity 校验:结构摘要比对,而不是单元测试。 见产物一致性指南

推论:能挪进纯 Python 层的逻辑就挪。一个函数只要不碰 bpy 放进 geom.py 就立刻获得测试覆盖的资格。

以 MeshBatch 为边界的静态设施测试

少数 bpy 要素模块的价值在于确定性地向 MeshBatch 追加顶点与面,而不是调用 bpy API 本身。对这类模块(例如 osmassets/traffic_signals.py),应在 blender/tests/ 用假的 osmassets.mesh.MeshBatch 导入模块,断言有效输入的装配数量和关键几何方向。这样可覆盖 “校验函数意外返回空、所有要素被静默跳过”这一类错误,不必依赖可用的 Blender 进程。

测试必须在本文件列出的 python3 -m unittest discover blender/tests 命令下独立运行;测试 文件自己添加 blender/sys.path,不能依赖其他测试的导入顺序。


反模式

反模式 后果
把当前输出粘成期望值 实现有 bug 时测试照样绿
新几何函数不配退化输入测试 真实数据一进来就崩
退化输入抛异常而不是返回空 一个坏多边形中断整片区域
带随机的函数不收 seed parity 校验永久红
mock 掉 OSM 解析 测不到它唯一的价值
在测试里 import bpy 侧模块 整个套件无法运行
只测一种绕向 反向多边形进来才发现