Skip to content

Commit 73bf36d

Browse files
committed
Add tests for metrics: refactor sampling rate tests and add Prometheus exporter mocks
1 parent 8211f2d commit 73bf36d

2 files changed

Lines changed: 96 additions & 17 deletions

File tree

tests/test_exporters.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,82 @@ def test_func(x):
464464
test_func.clear_cache()
465465

466466

467+
@pytest.mark.memory
468+
def test_prometheus_collector_collect_mocked():
469+
"""Test CachierCollector.collect() loop using mocked metric family types.
470+
471+
Covers lines 81-99 without requiring prometheus_client to be installed.
472+
473+
"""
474+
from unittest.mock import MagicMock, patch
475+
476+
from cachier.exporters.prometheus import CachierCollector
477+
478+
@cachier(backend="memory", enable_metrics=True)
479+
def test_func(x):
480+
return x * 2
481+
482+
test_func.clear_cache()
483+
test_func(5)
484+
test_func(5)
485+
486+
exporter = PrometheusExporter(port=0, use_prometheus_client=False)
487+
exporter.register_function(test_func)
488+
489+
with (
490+
patch("cachier.exporters.prometheus.CounterMetricFamily", lambda *a, **kw: MagicMock()),
491+
patch("cachier.exporters.prometheus.GaugeMetricFamily", lambda *a, **kw: MagicMock()),
492+
):
493+
collector = CachierCollector(exporter)
494+
results = list(collector.collect())
495+
# 5 counter families + 3 gauge families
496+
assert len(results) == 8
497+
498+
test_func.clear_cache()
499+
500+
501+
@pytest.mark.memory
502+
def test_prometheus_start_prometheus_server_mocked():
503+
"""Test _start_prometheus_server and its MetricsHandler without prometheus_client.
504+
505+
Covers lines 285-329 (start() prom branch, MetricsHandler.do_GET, log_message).
506+
507+
"""
508+
import sys
509+
import urllib.request
510+
from http.client import HTTPConnection
511+
from unittest.mock import MagicMock, patch
512+
513+
mock_exposition = MagicMock()
514+
mock_exposition.generate_latest.return_value = b"# mocked metrics"
515+
mock_exposition.CONTENT_TYPE_LATEST = "text/plain"
516+
517+
prom_mock = MagicMock()
518+
prom_mock.exposition = mock_exposition
519+
520+
exporter = PrometheusExporter(port=0, use_prometheus_client=False)
521+
# Manually inject prometheus state to trigger _start_prometheus_server path
522+
exporter._prom_client = prom_mock
523+
exporter._registry = MagicMock()
524+
525+
with patch.dict(sys.modules, {"prometheus_client": prom_mock, "prometheus_client.exposition": mock_exposition}):
526+
exporter.start()
527+
actual_port = exporter._server.server_address[1]
528+
assert exporter._server is not None
529+
try:
530+
response = urllib.request.urlopen(f"http://127.0.0.1:{actual_port}/metrics", timeout=5)
531+
assert b"# mocked metrics" in response.read()
532+
533+
conn = HTTPConnection("127.0.0.1", actual_port)
534+
conn.request("GET", "/notfound")
535+
resp = conn.getresponse()
536+
assert resp.status == 404
537+
conn.close()
538+
finally:
539+
exporter.stop()
540+
assert exporter._server is None
541+
542+
467543
@pytest.mark.memory
468544
def test_prometheus_collector_collect_skips_none_metrics():
469545
"""Test CachierCollector.collect() skips functions where metrics is None."""

tests/test_metrics.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -450,14 +450,20 @@ async def async_func(x):
450450

451451

452452
def test_metrics_zero_sampling_rate():
453-
"""Test that sampling_rate=0.0 records nothing."""
453+
"""Test that sampling_rate=0.0 records nothing for all record_* methods."""
454454
metrics = CacheMetrics(sampling_rate=0.0)
455-
for _ in range(100):
456-
metrics.record_hit()
457-
metrics.record_miss()
455+
metrics.record_hit()
456+
metrics.record_miss()
457+
metrics.record_stale_hit()
458+
metrics.record_wait_timeout()
459+
metrics.record_size_limit_rejection()
460+
metrics.record_latency(0.1)
458461
stats = metrics.get_stats()
459-
# With 0.0 rate nothing should be sampled
460462
assert stats.total_calls == 0
463+
assert stats.stale_hits == 0
464+
assert stats.wait_timeouts == 0
465+
assert stats.size_limit_rejections == 0
466+
assert stats.avg_latency_ms == 0.0
461467

462468

463469
def test_metrics_get_stats_zero_window():
@@ -490,18 +496,15 @@ def test_metrics_wait_timeout_direct():
490496
assert stats.wait_timeouts == 1
491497

492498

493-
def test_metrics_sampling_rate_zero_skips_all_methods():
494-
"""Test that sampling_rate=0.0 causes all record_* methods to skip recording."""
495-
metrics = CacheMetrics(sampling_rate=0.0)
496-
metrics.record_stale_hit()
497-
metrics.record_wait_timeout()
498-
metrics.record_size_limit_rejection()
499-
metrics.record_latency(0.1)
500-
stats = metrics.get_stats()
501-
assert stats.stale_hits == 0
502-
assert stats.wait_timeouts == 0
503-
assert stats.size_limit_rejections == 0
504-
assert stats.avg_latency_ms == 0.0
499+
def test_should_sample_deterministic():
500+
"""Test _should_sample returns True/False deterministically via mocking."""
501+
from unittest.mock import patch
502+
503+
metrics = CacheMetrics(sampling_rate=0.5)
504+
with patch.object(metrics._random, "random", return_value=0.1):
505+
assert metrics._should_sample() is True
506+
with patch.object(metrics._random, "random", return_value=0.9):
507+
assert metrics._should_sample() is False
505508

506509

507510
def test_metrics_context_manager():

0 commit comments

Comments
 (0)