Testing
PicoGL includes a comprehensive testing framework to ensure reliability and compatibility across different platforms and OpenGL versions.
Test Structure
The test suite is organized as follows:
tests/
├── test_vertex_array_object.py # VAO tests
├── test_vertex_buffer_group.py # Legacy VBO tests
├── test_meshdata.py # MeshData tests
├── test_glmesh.py # Modern GLMesh tests
├── test_legacy_glmesh.py # Legacy GLMesh tests
├── test_object_renderer.py # ObjectRenderer tests
└── test_texture_renderer.py # TextureRenderer tests
Running Tests
Basic Test Execution
Run all tests:
python -m unittest discover tests
Run specific test file:
python -m unittest tests.test_vertex_array_object
Run specific test method:
python -m unittest tests.test_vertex_array_object.TestVertexArrayObject.test_initialization
Run with verbose output:
python -m unittest discover tests -v
Using pytest
Install pytest:
pip install pytest pytest-cov
Run tests with pytest:
pytest tests/
Run with coverage:
pytest tests/ --cov=picogl
Run specific test:
pytest tests/test_vertex_array_object.py::TestVertexArrayObject::test_initialization
Test Categories
Unit Tests
Unit tests verify individual components in isolation:
VertexArrayObject Tests:
class TestVertexArrayObject(unittest.TestCase):
def test_initialization(self):
vao = VertexArrayObject()
self.assertIsNotNone(vao)
def test_add_vbo(self):
vao = VertexArrayObject()
vao.add_vbo(index=0, data=vertices, size=3)
self.assertEqual(len(vao.vbos), 1)
def test_add_ebo(self):
vao = VertexArrayObject()
vao.add_ebo(data=indices)
self.assertIsNotNone(vao.ebo)
MeshData Tests:
class TestMeshData(unittest.TestCase):
def test_from_raw(self):
data = MeshData.from_raw(vertices=vertices, colors=colors)
self.assertIsNotNone(data)
self.assertEqual(data.vertex_count, len(vertices) // 3)
def test_draw(self):
data = MeshData.from_raw(vertices=vertices, colors=colors)
data.draw() # Should not raise exception
Integration Tests
Integration tests verify component interactions:
Renderer Integration:
class TestRendererIntegration(unittest.TestCase):
def test_object_renderer_initialization(self):
context = GLContext()
data = MeshData.from_raw(vertices=vertices, colors=colors)
renderer = ObjectRenderer(context=context, data=data)
renderer.initialize()
self.assertTrue(renderer.initialized)
def test_texture_renderer_initialization(self):
context = GLContext()
data = MeshData.from_raw(vertices=vertices, uvs=uvs)
renderer = TextureRenderer(context=context, data=data)
renderer.initialize_textures()
self.assertIsNotNone(renderer.texture)
Performance Tests
Performance tests verify rendering performance:
Rendering Performance:
class TestRenderingPerformance(unittest.TestCase):
def test_render_performance(self):
start_time = time.time()
for _ in range(1000):
renderer.render()
end_time = time.time()
frame_time = (end_time - start_time) / 1000
self.assertLess(frame_time, 0.016) # 60 FPS
def test_memory_usage(self):
initial_memory = psutil.Process().memory_info().rss
# Create and use renderer
renderer = create_renderer()
renderer.render()
final_memory = psutil.Process().memory_info().rss
memory_increase = final_memory - initial_memory
self.assertLess(memory_increase, 100 * 1024 * 1024) # 100MB
Compatibility Tests
Compatibility tests verify cross-platform functionality:
OpenGL Version Compatibility:
class TestOpenGLCompatibility(unittest.TestCase):
def test_modern_opengl(self):
if has_modern_opengl():
vao = VertexArrayObject()
self.assertIsNotNone(vao)
def test_legacy_opengl(self):
if has_legacy_opengl():
vbg = VertexBufferGroup()
self.assertIsNotNone(vbg)
def test_immediate_mode(self):
# Test immediate mode fallback
pass
Test Mocking
OpenGL Mocking
Since OpenGL requires a graphics context, tests use mocking:
Global OpenGL Mocking:
class TestVertexArrayObject(unittest.TestCase):
def setUp(self):
# Mock all OpenGL functions
self.gl_patches = [
patch('picogl.backend.modern.core.vertex.array.object.glGenVertexArrays'),
patch('picogl.backend.modern.core.vertex.array.object.glBindVertexArray'),
patch('picogl.backend.modern.core.vertex.array.object.glDeleteVertexArrays'),
# ... more patches
]
for patch_obj in self.gl_patches:
patch_obj.start()
def tearDown(self):
for patch_obj in self.gl_patches:
patch_obj.stop()
Specific Function Mocking:
def test_error_handling(self):
with patch('picogl.backend.modern.core.vertex.array.object.glGenVertexArrays') as mock_gen:
mock_gen.side_effect = OpenGLError("Context not available")
with self.assertRaises(OpenGLError):
VertexArrayObject()
Dependency Mocking
Mock external dependencies:
NumPy Mocking:
@patch('numpy.array')
def test_array_creation(self, mock_array):
mock_array.return_value = np.array([1, 2, 3])
result = create_vertex_array()
mock_array.assert_called_once()
PIL Mocking:
@patch('PIL.Image.open')
def test_texture_loading(self, mock_open):
mock_image = MagicMock()
mock_open.return_value = mock_image
texture = load_texture("test.png")
mock_open.assert_called_once_with("test.png")
Test Data
Test Fixtures
Create reusable test data:
Vertex Data:
class TestData:
@staticmethod
def create_triangle_vertices():
return np.array([
[0, 0, 0],
[1, 0, 0],
[0, 1, 0]
], dtype=np.float32)
@staticmethod
def create_triangle_colors():
return np.array([
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
], dtype=np.float32)
@staticmethod
def create_triangle_faces():
return np.array([
[0, 1, 2]
], dtype=np.uint32)
Mock Objects:
class MockGLContext:
def __init__(self):
self.vaos = {}
self.shader = MagicMock()
self.mvp_matrix = np.identity(4, dtype=np.float32)
def create_shader_program(self, vertex_file, fragment_file):
self.shader = MagicMock()
return self.shader
Test Utilities
Create helper functions for testing:
OpenGL Context Testing:
def create_mock_opengl_context():
"""Create a mock OpenGL context for testing."""
with patch('OpenGL.GL.glGetString') as mock_get_string:
mock_get_string.return_value = b"OpenGL 3.3.0"
return mock_get_string
Error Testing:
def test_with_gl_error(error_code, expected_exception):
"""Test function with specific OpenGL error."""
with patch('OpenGL.GL.glGetError') as mock_get_error:
mock_get_error.return_value = error_code
with pytest.raises(expected_exception):
function_that_uses_opengl()
Continuous Integration
GitHub Actions
Test Workflow:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: [3.7, 3.8, 3.9, '3.10', '3.11']
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
pip install -e .[dev]
- name: Run tests
run: |
python -m unittest discover tests
- name: Run coverage
run: |
pytest tests/ --cov=picogl --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v1
Build Workflow:
name: Build
on: [push, release]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.9'
- name: Install dependencies
run: |
pip install build twine
- name: Build package
run: |
python -m build
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: dist
path: dist/
Test Coverage
Coverage Requirements
Minimum Coverage: * Overall: 80% * Critical modules: 90% * New code: 95%
Coverage Exclusions: * Test files * Example files * Platform-specific code * Fallback implementations
Coverage Report:
pytest tests/ --cov=picogl --cov-report=html --cov-report=term
Coverage Configuration:
[coverage:run]
source = picogl
omit =
*/tests/*
*/examples/*
*/__pycache__/*
*/legacy/*
[coverage:report]
exclude_lines =
pragma: no cover
def __repr__
raise AssertionError
raise NotImplementedError
Code Quality
Linting
flake8 Configuration:
[flake8]
max-line-length = 88
extend-ignore = E203, W503
exclude =
.git,
__pycache__,
.venv,
_build,
dist
Run Linting:
flake8 picogl/
flake8 tests/
Type Checking
mypy Configuration:
[mypy]
python_version = 3.7
warn_return_any = True
warn_unused_configs = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_no_return = True
warn_unreachable = True
strict_equality = True
Run Type Checking:
mypy picogl/
mypy tests/
Formatting
black Configuration:
[tool.black]
line-length = 88
target-version = ['py37']
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
Run Formatting:
black picogl/
black tests/
Test Best Practices
Writing Tests
Test Naming:
def test_initialization_with_valid_data(self):
"""Test initialization with valid input data."""
pass
def test_initialization_raises_error_with_invalid_data(self):
"""Test initialization raises error with invalid data."""
pass
Test Structure:
def test_functionality(self):
# Arrange
input_data = create_test_data()
expected_result = create_expected_result()
# Act
actual_result = function_under_test(input_data)
# Assert
self.assertEqual(actual_result, expected_result)
Test Documentation:
def test_complex_functionality(self):
"""Test complex functionality with multiple inputs.
This test verifies that the function handles:
- Valid input data
- Edge cases
- Error conditions
Expected behavior:
- Returns correct result for valid input
- Handles edge cases gracefully
- Raises appropriate errors for invalid input
"""
pass
Test Maintenance
Keep Tests Simple: * One assertion per test * Clear test names * Minimal setup
Avoid Test Dependencies: * Tests should be independent * No shared state * Clean up after tests
Regular Test Updates: * Update tests when code changes * Remove obsolete tests * Add tests for new features
Test Performance: * Keep tests fast * Use mocking to avoid slow operations * Parallel test execution when possible
Debugging Tests
Verbose Output:
python -m unittest discover tests -v
Debug Specific Test:
import pdb
def test_debugging(self):
pdb.set_trace() # Set breakpoint
# Test code here
Test Isolation:
def test_isolated(self):
# Use fresh instances
context = GLContext()
data = MeshData.from_raw(vertices=vertices, colors=colors)
# Test without side effects
For more information about testing, see the Development guide.