Thanks to visit codestin.com
Credit goes to tamnd.github.io

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.