17.23 Testing Geometry Algorithms

Geometry code fails in ways that ordinary examples rarely expose.

17.23 Testing Geometry Algorithms

Problem

Geometry code fails in ways that ordinary examples rarely expose.

A convex hull may work for a random cloud of points but fail when all points are collinear. A segment intersection routine may pass crossing cases but fail on shared endpoints. A point-in-polygon test may classify ordinary points correctly but break when the query point lies exactly on a vertex.

Testing geometry algorithms requires deliberate coverage of:

degenerate cases
boundary cases
large coordinates
small coordinates
near-degenerate floating-point cases
randomized inputs
algorithmic invariants

The goal is to test both the result and the assumptions behind the result.

Solution

Build a layered test strategy.

Use:

small hand-written examples
boundary-case tests
brute-force reference implementations
property-based tests
metamorphic tests
random stress tests

Do not rely only on visual inspection. Geometry can look correct when the topology is wrong.

Start with Deterministic Examples

For a segment intersection function, write explicit tests for each case in the specification.

from dataclasses import dataclass

@dataclass(frozen=True)
class Point:
    x: int
    y: int

Example tests:

def test_segments_cross():
    a = Point(0, 0)
    b = Point(4, 4)
    c = Point(0, 4)
    d = Point(4, 0)

    assert segments_intersect(a, b, c, d)

def test_segments_share_endpoint():
    a = Point(0, 0)
    b = Point(4, 0)
    c = Point(4, 0)
    d = Point(8, 0)

    assert segments_intersect(a, b, c, d)

def test_segments_parallel_disjoint():
    a = Point(0, 0)
    b = Point(4, 0)
    c = Point(0, 1)
    d = Point(4, 1)

    assert not segments_intersect(a, b, c, d)

def test_segments_collinear_overlap():
    a = Point(0, 0)
    b = Point(6, 0)
    c = Point(2, 0)
    d = Point(8, 0)

    assert segments_intersect(a, b, c, d)

These tests correspond directly to proof cases.

Test Empty and Small Inputs

Many geometry algorithms have special behavior for small inputs.

Convex hull examples:

def test_hull_empty():
    assert convex_hull([]) == []

def test_hull_one_point():
    p = Point(1, 2)

    assert convex_hull([p]) == [p]

def test_hull_two_points():
    a = Point(0, 0)
    b = Point(1, 1)

    assert convex_hull([b, a]) == [a, b]

Closest pair:

def test_closest_pair_empty():
    assert closest_pair([]) is None

def test_closest_pair_one_point():
    assert closest_pair([Point(0, 0)]) is None

These cases often reveal assumptions hidden in loops and indexing logic.

Test Degenerate Inputs

Degenerate inputs are geometrically valid but structurally special.

Examples:

duplicate points
all points collinear
zero-length segments
zero-area rectangles
polygon vertices repeated
horizontal and vertical edges

Convex hull tests:

def test_hull_all_collinear():
    points = [
        Point(0, 0),
        Point(1, 1),
        Point(2, 2),
        Point(3, 3),
    ]

    assert convex_hull(points) == [
        Point(0, 0),
        Point(3, 3),
    ]

def test_hull_duplicate_points():
    points = [
        Point(0, 0),
        Point(0, 0),
        Point(1, 0),
        Point(0, 1),
    ]

    assert set(convex_hull(points)) == {
        Point(0, 0),
        Point(1, 0),
        Point(0, 1),
    }

For point-in-polygon, test boundary classification explicitly.

def test_point_on_polygon_edge():
    polygon = [
        Point(0, 0),
        Point(4, 0),
        Point(4, 4),
        Point(0, 4),
    ]

    assert classify_point_in_polygon(
        Point(2, 0),
        polygon
    ) == "boundary"

Use Brute Force as an Oracle

Optimized geometry algorithms are often hard to inspect directly.

A brute-force algorithm can act as a reference for small inputs.

For closest pair:

def closest_pair_brute(points):
    if len(points) < 2:
        return None

    best = None
    best_distance = None

    for i in range(len(points)):
        for j in range(i + 1, len(points)):
            d = distance_squared(points[i], points[j])

            if best_distance is None or d < best_distance:
                best_distance = d
                best = (points[i], points[j])

    return best, best_distance

Then compare the optimized algorithm against it:

def test_closest_pair_against_brute(points):
    expected = closest_pair_brute(points)
    actual = closest_pair(points)

    assert actual[1] == expected[1]

This approach is especially effective with randomized small inputs.

Randomized Stress Testing

Generate many small inputs and compare optimized algorithms to brute-force versions.

import random

def random_points(count, low=-10, high=10):
    return [
        Point(
            random.randint(low, high),
            random.randint(low, high)
        )
        for _ in range(count)
    ]

Stress test:

def test_closest_pair_random():
    for _ in range(1000):
        points = random_points(20)

        expected = closest_pair_brute(points)
        actual = closest_pair(points)

        if expected is None:
            assert actual is None
        else:
            assert actual[1] == expected[1]

Random testing is valuable because geometry has many configurations that are easy to miss by hand.

Property-Based Tests

Some properties should always hold, even when you do not know the exact answer.

For convex hull:

every input point lies inside or on the hull
every hull vertex comes from the input
the hull is convex

Example checks:

def is_convex_polygon(points):
    if len(points) <= 2:
        return True

    signs = []

    for i in range(len(points)):
        a = points[i]
        b = points[(i + 1) % len(points)]
        c = points[(i + 2) % len(points)]

        value = orientation(a, b, c)

        if value != 0:
            signs.append(value > 0)

    return all(sign == signs[0] for sign in signs)

Then:

def test_hull_is_convex():
    for _ in range(1000):
        points = random_points(30)
        hull = convex_hull(points)

        assert is_convex_polygon(hull)

This does not fully prove correctness, but it catches many errors.

Metamorphic Testing

A metamorphic test transforms the input in a way that should preserve the answer structure.

Examples:

translate all points
scale all coordinates
shuffle input order
duplicate input points
rotate a point set

For convex hull, shuffling input order should not change the hull set.

def test_hull_input_order_does_not_matter():
    points = random_points(50)

    shuffled = points[:]
    random.shuffle(shuffled)

    assert set(convex_hull(points)) == set(convex_hull(shuffled))

Translation should preserve shape:

def translate(points, dx, dy):
    return [
        Point(p.x + dx, p.y + dy)
        for p in points
    ]

Test:

def test_hull_translation():
    points = random_points(50)
    moved = translate(points, 100, -200)

    hull = convex_hull(points)
    moved_hull = convex_hull(moved)

    assert set(translate(hull, 100, -200)) == set(moved_hull)

Metamorphic tests are useful when computing an exact expected answer is inconvenient.

Test Numeric Extremes

Integer geometry should be tested near overflow boundaries.

def test_large_coordinates_orientation():
    a = Point(10**9, 10**9)
    b = Point(2 * 10**9, 10**9)
    c = Point(10**9, 2 * 10**9)

    assert orientation(a, b, c) > 0

Floating-point geometry should include near-degenerate cases:

def test_nearly_collinear_points():
    a = Point(0.0, 0.0)
    b = Point(1e9, 1e9)
    c = Point(2e9, 2e9 + 1e-6)

    result = orientation_sign(a, b, c)

    assert result in (-1, 0, 1)

The expected classification depends on your tolerance policy. The key is to document and test that policy.

Test Event Ordering

Sweep-line algorithms are sensitive to same-coordinate events.

For rectangle union:

def test_touching_rectangles_do_not_double_count():
    rectangles = [
        Rectangle(0, 0, 2, 2),
        Rectangle(2, 0, 4, 2),
    ]

    assert rectangle_union_area(rectangles) == 8

For overlapping rectangles:

def test_overlapping_rectangles_count_overlap_once():
    rectangles = [
        Rectangle(0, 0, 4, 4),
        Rectangle(2, 2, 6, 6),
    ]

    assert rectangle_union_area(rectangles) == 28

These cases test boundary semantics directly.

Test Against Known Identities

Some geometric formulas have identities that can be checked.

For polygon area:

area should be unchanged by translation
area should change by factor s² under scaling by s
reversing vertex order should negate signed area

Example:

def test_signed_area_reversal():
    polygon = [
        Point(0, 0),
        Point(4, 0),
        Point(4, 3),
        Point(0, 3),
    ]

    assert signed_area(polygon) == -signed_area(list(reversed(polygon)))

This style catches mistakes in orientation and vertex ordering.

Common Pitfalls

Testing Only Random Inputs

Random inputs rarely include exact degeneracies.

Testing Only Hand-Written Inputs

Hand-written tests rarely cover enough configurations.

Ignoring Boundary Semantics

Tests should explicitly cover edge, vertex, and touching cases.

Comparing Floating Values Too Strictly

Use tolerance-aware comparisons for measurements.

Using the Same Bug in the Oracle

A reference implementation should be structurally different from the optimized implementation.

Practical Test Suite Structure

A useful geometry test suite usually contains:

test_predicates.py
test_segments.py
test_polygons.py
test_hulls.py
test_sweep.py
test_spatial_index.py
test_numeric_robustness.py

Predicates should be tested first because many algorithms depend on them.

When a higher-level algorithm fails, a passing predicate suite narrows the search.

Discussion

Testing geometry algorithms means testing mathematics, implementation, and specification at the same time. Small deterministic cases confirm the intended boundary behavior. Brute-force oracles validate optimized algorithms. Random and property-based tests broaden coverage. Metamorphic tests verify invariants that should survive transformations.

The strongest test suites mirror the correctness proof. If the proof splits a problem into cases, the test suite should contain those cases. If the proof relies on an invariant, the test suite should check that invariant directly where possible.