Testing Guide for Geomstats#
This guide will help you write tests for geomstats, whether you’re fixing a bug, adding a new feature, or contributing for the first time.
Quick Start: Your First Test#
If you need to write a simple test (e.g., for a bug fix or small feature), you can write it directly without understanding the full testing infrastructure.
Basic Example#
Create or find the test file corresponding to your module:
# In tests/tests_geomstats/test_geometry/test_my_space.py
import pytest
import geomstats.backend as gs
from geomstats.geometry.my_space import MySpace
def test_my_bugfix():
"""Test that issue #123 is fixed."""
space = MySpace(dim=3)
point = gs.array([1.0, 2.0, 3.0])
result = space.my_function(point)
expected = gs.array([2.0, 4.0, 6.0])
gs.testing.assert_allclose(result, expected)
Run your test:
$ pytest tests/tests_geomstats/test_geometry/test_my_space.py::test_my_bugfix
When to Use Simple Tests#
Use simple, direct tests for:
Bug fixes - Add a regression test that would have caught the bug
Edge cases - Test specific corner cases you discovered
Quick validation - Test a specific behavior or property
Learning - When you’re new to the project
These tests don’t need vectorization, parametrization, or the data-driven architecture. Just write clear, focused tests.
Understanding Test Organization#
Geomstats has 110+ test files organized by domain:
tests/tests_geomstats/
├── test_geometry/ # Tests for manifolds and spaces
│ ├── test_euclidean.py
│ ├── test_hypersphere.py
│ ├── test_special_orthogonal.py
│ └── data/ # Test data for parametrized tests
│ ├── euclidean.py
│ └── special_orthogonal.py
├── test_learning/ # Machine learning algorithms
├── test_distributions/ # Probability distributions
└── test_numerics/ # Numerical methods
Finding the Right Test File#
Rule of thumb: If you modified geomstats/geometry/my_space.py,
add tests to tests/tests_geomstats/test_geometry/test_my_space.py.
If the test file doesn’t exist, create it following the naming convention:
test_<module_name>.py
Writing Tests: Step by Step#
Step 1: Import What You Need#
import pytest
import geomstats.backend as gs
from geomstats.geometry.my_space import MySpace
Always use
geomstats.backend as gsfor backend compatibilityImport the classes/functions you’re testing
Step 2: Write a Test Function#
Test function names must start with test_:
def test_belongs_returns_true_for_valid_point():
"""Test that belongs() returns True for points on the manifold."""
space = MySpace(dim=3)
point = space.random_point()
result = space.belongs(point)
assert result is True
Good practices:
Use descriptive names:
test_belongs_returns_true_for_valid_pointrather thantest_1Add a docstring explaining what you’re testing
One assertion per test (when possible)
Test one behavior at a time
Step 3: Use Appropriate Assertions#
For backend-compatible assertions, use:
# For numerical comparisons (handles floating point)
gs.testing.assert_allclose(result, expected, atol=1e-6)
# For exact equality
assert gs.all(result == expected)
# For boolean results
assert result is True
assert result is False
# For shapes
assert result.shape == (3, 3)
Don’t use == directly for arrays, as it won’t work correctly
with PyTorch tensors.
Step 4: Add Test Markers#
Use pytest markers to categorize your tests:
@pytest.mark.smoke
def test_identity_is_identity():
"""Smoke test: quick sanity check."""
space = MySpace()
identity = space.identity
assert gs.all(identity == gs.eye(3))
@pytest.mark.random
def test_exp_log_inverse(n_points=5):
"""Test with random data."""
space = MySpace()
points = space.random_point(n_points)
# ... test logic
@pytest.mark.slow
def test_expensive_computation():
"""Mark slow tests to skip in quick test runs."""
# ... expensive test
Common markers:
@pytest.mark.smoke- Fast, basic tests for CI@pytest.mark.random- Tests using random data@pytest.mark.slow- Slow tests (run less frequently)@pytest.mark.vec- Vectorization tests (auto-generated)
Running Your Tests#
Run a specific test:
$ pytest tests/tests_geomstats/test_geometry/test_my_space.py::test_my_function
Run all tests in a file:
$ pytest tests/tests_geomstats/test_geometry/test_my_space.py
Run all tests in a directory:
$ pytest tests/tests_geomstats/test_geometry/
Run only smoke tests (fast):
$ pytest -m smoke
Run with verbose output:
$ pytest -v tests/tests_geomstats/test_geometry/test_my_space.py
Run with backend selection:
$ GEOMSTATS_BACKEND=pytorch pytest tests/tests_geomstats/test_geometry/test_my_space.py
Testing Best Practices#
Test What Could Go Wrong#
Think about edge cases:
def test_distance_is_zero_for_same_point():
"""Distance from a point to itself should be zero."""
space = MySpace()
point = space.random_point()
distance = space.distance(point, point)
gs.testing.assert_allclose(distance, 0.0, atol=1e-10)
def test_distance_is_symmetric():
"""Distance should be symmetric: d(a,b) = d(b,a)."""
space = MySpace()
point_a = space.random_point()
point_b = space.random_point()
dist_ab = space.distance(point_a, point_b)
dist_ba = space.distance(point_b, point_a)
gs.testing.assert_allclose(dist_ab, dist_ba)
Test Mathematical Properties#
For geometric objects, test mathematical properties:
@pytest.mark.mathprop
def test_exp_log_composition_is_identity():
"""Test that exp(log(point, base), base) == point."""
space = MySpace()
base_point = space.random_point()
point = space.random_point()
log_result = space.log(point, base_point)
exp_result = space.exp(log_result, base_point)
gs.testing.assert_allclose(exp_result, point, atol=1e-6)
Use Parametrization for Multiple Cases#
Test multiple scenarios efficiently:
@pytest.mark.parametrize("dim", [2, 3, 5, 10])
def test_identity_has_correct_shape(dim):
"""Test identity matrix has shape (dim, dim)."""
space = MySpace(dim=dim)
identity = space.identity
assert identity.shape == (dim, dim)
@pytest.mark.parametrize("point,expected", [
(gs.array([1.0, 0.0]), True),
(gs.array([0.0, 1.0]), True),
(gs.array([2.0, 0.0]), False),
(gs.array([0.0, 0.0]), False),
])
def test_belongs_specific_cases(point, expected):
"""Test belongs for specific known cases."""
space = UnitCircle()
result = space.belongs(point)
assert result == expected
Test Coverage Requirements#
Geomstats requires 90% test coverage for new code. This means:
Every public function should have at least one test
Test the main code path and error cases
Don’t test private functions (starting with
_) directly
Check coverage locally:
$ pytest --cov=geomstats tests/tests_geomstats/test_geometry/test_my_space.py
$ pytest --cov=geomstats --cov-report=html tests/
Common Testing Patterns#
Testing a New Manifold#
When adding a new manifold, test these core methods:
def test_belongs():
"""Test that belongs correctly identifies points on the manifold."""
pass
def test_random_point_belongs():
"""Test that random_point generates valid points."""
space = MySpace()
point = space.random_point()
assert space.belongs(point)
def test_projection_belongs():
"""Test that projection returns a point on the manifold."""
space = MySpace()
point = gs.random.normal(size=space.shape)
projected = space.projection(point)
assert space.belongs(projected)
def test_dimension():
"""Test the dimension property."""
space = MySpace(dim=5)
assert space.dim == 5
Testing a New Metric#
For Riemannian metrics:
def test_metric_is_positive():
"""Test that metric(v, v) >= 0."""
pass
def test_metric_is_symmetric():
"""Test that metric(u, v) == metric(v, u)."""
pass
def test_exp_belongs():
"""Test that exp returns a point on the manifold."""
pass
def test_log_belongs():
"""Test that log returns a tangent vector."""
pass
When Simple Tests Aren’t Enough#
If you need to test vectorization (batch operations) or need comprehensive testing across many parameter combinations, see:
Testing Architecture - Understanding the data-driven test infrastructure
Testing Quick Reference - Quick reference for advanced testing patterns
These guides explain the three-layer testing architecture used in geomstats:
Test Data Layer (in
data/subdirectories) - Defines test casesTest Case Layer (in
geomstats/test_cases/) - Reusable test logicConcrete Test Layer (in
tests/tests_geomstats/) - Instantiates tests
This architecture enables automatic vectorization testing and massive code reuse.
Getting Help#
If you’re stuck:
Look at similar existing tests for patterns
Ask on GitHub issues or discussions
Reach out on the mailing list: hi@geomstats.ai
Check the Contributing Guide guide for general contribution help
Remember: It’s okay to start with simple tests! You can always refactor to more sophisticated patterns later. The most important thing is having tests that validate your code works correctly.