Skip to content

Commit 9e1e62d

Browse files
committed
rework tests
1 parent 9c79200 commit 9e1e62d

15 files changed

+2037
-0
lines changed

tests/conftest.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,26 @@
225225
machine_learning_functions = []
226226
HAS_ML = False
227227

228+
# Visualization dependencies
229+
try:
230+
import plotly.graph_objects as go
231+
import matplotlib.pyplot as plt
232+
233+
HAS_VIZ = True
234+
except ImportError:
235+
HAS_VIZ = False
236+
go = None
237+
plt = None
238+
239+
# Streamlit testing
240+
try:
241+
from streamlit.testing.v1 import AppTest
242+
243+
HAS_STREAMLIT = True
244+
except ImportError:
245+
HAS_STREAMLIT = False
246+
AppTest = None
247+
228248
# BBOB as list
229249
BBOB_FUNCTION_LIST = list(BBOB_FUNCTIONS.values())
230250

@@ -271,6 +291,16 @@
271291
reason="Requires CEC data: pip install surfaces[cec]"
272292
)
273293

294+
requires_viz = pytest.mark.skipif(
295+
not HAS_VIZ,
296+
reason="Requires visualization deps: pip install surfaces[viz]"
297+
)
298+
299+
requires_streamlit = pytest.mark.skipif(
300+
not HAS_STREAMLIT,
301+
reason="Requires streamlit: pip install surfaces[dashboard]"
302+
)
303+
274304

275305
# =============================================================================
276306
# Helper Functions
@@ -443,6 +473,8 @@ def pytest_configure(config):
443473
config.addinivalue_line("markers", "algebraic: Algebraic/mathematical functions")
444474
config.addinivalue_line("markers", "engineering: Engineering design functions")
445475
config.addinivalue_line("markers", "requires_data: Requires external data files")
476+
config.addinivalue_line("markers", "viz: Visualization tests (require plotly/matplotlib)")
477+
config.addinivalue_line("markers", "dashboard: Streamlit dashboard tests")
446478

447479

448480
def pytest_collection_modifyitems(config, items):
@@ -477,3 +509,12 @@ def pytest_collection_modifyitems(config, items):
477509

478510
if "smoke" in test_path:
479511
item.add_marker(pytest.mark.smoke)
512+
513+
if "visualization" in test_path:
514+
item.add_marker(pytest.mark.viz)
515+
516+
if "dashboard" in test_path:
517+
item.add_marker(pytest.mark.dashboard)
518+
519+
if "test_optimization" in test_path:
520+
item.add_marker(pytest.mark.slow)

tests/dashboard/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Dashboard tests - headless Streamlit tests using AppTest."""

tests/dashboard/test_app.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Tests for Streamlit dashboard app using AppTest.
6+
7+
These tests run the dashboard in headless mode without a browser.
8+
Streamlit's AppTest allows testing UI components programmatically.
9+
"""
10+
11+
import pytest
12+
import tempfile
13+
from pathlib import Path
14+
from unittest.mock import patch, MagicMock
15+
16+
from tests.conftest import requires_streamlit, HAS_STREAMLIT
17+
18+
if HAS_STREAMLIT:
19+
from streamlit.testing.v1 import AppTest
20+
from surfaces._surrogates._dashboard.database import init_db
21+
22+
23+
@pytest.fixture
24+
def mock_db():
25+
"""Create a temporary database and mock the DB_PATH."""
26+
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
27+
db_path = Path(f.name)
28+
init_db(db_path)
29+
yield db_path
30+
db_path.unlink(missing_ok=True)
31+
32+
33+
# =============================================================================
34+
# App Loading Tests
35+
# =============================================================================
36+
37+
38+
@pytest.mark.dashboard
39+
@requires_streamlit
40+
class TestAppLoading:
41+
"""Tests for basic app loading."""
42+
43+
def test_app_runs_without_error(self, mock_db):
44+
"""App initializes and runs without exceptions."""
45+
with patch(
46+
"surfaces._surrogates._dashboard.database.DB_PATH", mock_db
47+
), patch(
48+
"surfaces._surrogates._dashboard.app.init_db"
49+
), patch(
50+
"surfaces._surrogates._dashboard.app.sync_all",
51+
return_value={"synced": 0},
52+
):
53+
at = AppTest.from_file(
54+
"src/surfaces/_surrogates/_dashboard/app.py",
55+
default_timeout=10,
56+
)
57+
at.run()
58+
59+
assert not at.exception, f"App raised exception: {at.exception}"
60+
61+
def test_app_has_title(self, mock_db):
62+
"""App displays the dashboard title."""
63+
with patch(
64+
"surfaces._surrogates._dashboard.database.DB_PATH", mock_db
65+
), patch(
66+
"surfaces._surrogates._dashboard.app.init_db"
67+
), patch(
68+
"surfaces._surrogates._dashboard.app.sync_all",
69+
return_value={"synced": 0},
70+
):
71+
at = AppTest.from_file(
72+
"src/surfaces/_surrogates/_dashboard/app.py",
73+
default_timeout=10,
74+
)
75+
at.run()
76+
77+
assert len(at.title) > 0
78+
assert "Surrogate Dashboard" in at.title[0].value
79+
80+
81+
# =============================================================================
82+
# Sidebar Tests
83+
# =============================================================================
84+
85+
86+
@pytest.mark.dashboard
87+
@requires_streamlit
88+
class TestSidebar:
89+
"""Tests for sidebar components."""
90+
91+
def test_sidebar_has_metrics(self, mock_db):
92+
"""Sidebar displays quick stats metrics."""
93+
with patch(
94+
"surfaces._surrogates._dashboard.database.DB_PATH", mock_db
95+
), patch(
96+
"surfaces._surrogates._dashboard.app.init_db"
97+
), patch(
98+
"surfaces._surrogates._dashboard.app.sync_all",
99+
return_value={"synced": 0},
100+
), patch(
101+
"surfaces._surrogates._dashboard.app.get_dashboard_stats",
102+
return_value={
103+
"total_functions": 10,
104+
"with_surrogate": 7,
105+
"without_surrogate": 3,
106+
"total_validations": 20,
107+
"total_trainings": 5,
108+
},
109+
):
110+
at = AppTest.from_file(
111+
"src/surfaces/_surrogates/_dashboard/app.py",
112+
default_timeout=10,
113+
)
114+
at.run()
115+
116+
# Check that metrics are rendered
117+
assert len(at.metric) >= 4
118+
119+
def test_sidebar_has_sync_button(self, mock_db):
120+
"""Sidebar has a sync database button."""
121+
with patch(
122+
"surfaces._surrogates._dashboard.database.DB_PATH", mock_db
123+
), patch(
124+
"surfaces._surrogates._dashboard.app.init_db"
125+
), patch(
126+
"surfaces._surrogates._dashboard.app.sync_all",
127+
return_value={"synced": 0},
128+
):
129+
at = AppTest.from_file(
130+
"src/surfaces/_surrogates/_dashboard/app.py",
131+
default_timeout=10,
132+
)
133+
at.run()
134+
135+
# Find the sync button
136+
buttons = [b for b in at.button if "Sync" in str(b.label)]
137+
assert len(buttons) > 0
138+
139+
140+
# =============================================================================
141+
# Tab Navigation Tests
142+
# =============================================================================
143+
144+
145+
@pytest.mark.dashboard
146+
@requires_streamlit
147+
class TestTabNavigation:
148+
"""Tests for tab-based navigation."""
149+
150+
def test_app_has_tabs(self, mock_db):
151+
"""App displays the four main tabs."""
152+
with patch(
153+
"surfaces._surrogates._dashboard.database.DB_PATH", mock_db
154+
), patch(
155+
"surfaces._surrogates._dashboard.app.init_db"
156+
), patch(
157+
"surfaces._surrogates._dashboard.app.sync_all",
158+
return_value={"synced": 0},
159+
):
160+
at = AppTest.from_file(
161+
"src/surfaces/_surrogates/_dashboard/app.py",
162+
default_timeout=10,
163+
)
164+
at.run()
165+
166+
# Check that tabs exist
167+
assert len(at.tabs) > 0
168+
169+
170+
# =============================================================================
171+
# Integration Tests
172+
# =============================================================================
173+
174+
175+
@pytest.mark.dashboard
176+
@requires_streamlit
177+
@pytest.mark.slow
178+
class TestIntegration:
179+
"""Integration tests with actual database operations."""
180+
181+
def test_full_workflow(self, mock_db):
182+
"""Test complete workflow: load, display stats, interact."""
183+
from surfaces._surrogates._dashboard.database import (
184+
upsert_surrogate,
185+
insert_validation_run,
186+
)
187+
188+
# Populate test data
189+
upsert_surrogate(
190+
"TestClassifier",
191+
"classification",
192+
True,
193+
metadata={"n_samples": 1000, "training_r2": 0.96},
194+
db_path=mock_db,
195+
)
196+
insert_validation_run(
197+
"TestClassifier",
198+
"random",
199+
100,
200+
{"r2": 0.95, "mae": 0.01},
201+
{"avg_real_ms": 10, "avg_surrogate_ms": 0.1},
202+
db_path=mock_db,
203+
)
204+
205+
with patch(
206+
"surfaces._surrogates._dashboard.database.DB_PATH", mock_db
207+
), patch(
208+
"surfaces._surrogates._dashboard.app.init_db"
209+
), patch(
210+
"surfaces._surrogates._dashboard.app.sync_all",
211+
return_value={"synced": 1},
212+
):
213+
at = AppTest.from_file(
214+
"src/surfaces/_surrogates/_dashboard/app.py",
215+
default_timeout=15,
216+
)
217+
at.run()
218+
219+
# Verify app loaded without errors
220+
assert not at.exception
221+
222+
# Stats should reflect the test data
223+
stats_metrics = at.metric
224+
assert len(stats_metrics) >= 1

0 commit comments

Comments
 (0)