python-test-micrometer-testing-metrics
npx skills add https://github.com/dawiddutoit/custom-claude --skill python-test-micrometer-testing-metrics
Agent 安装分布
Skill 文档
Micrometer Testing Metrics
Table of Contents
Quick Start â What Is This | When to Use | Simple Example
How to Implement â Step-by-Step | Test Registry Setup | Examples
Help â Anti-Patterns | Requirements
Reference â Related Skills
Purpose
Testing metrics requires explicit registry setup since Micrometer doesn’t auto-wire in unit tests. This skill shows how to verify that custom metrics are correctly recorded in unit tests (SimpleMeterRegistry), integration tests (@AutoConfigureMetrics), and percentile/histogram calculations.
When to Use
Use this skill when:
- Writing unit tests for services that record metrics
- Validating metric values after business operations
- Testing percentiles and histogram configurations
- Asserting metric behavior without full Spring context
- Verifying counter increments, timer durations, or gauge values
- Testing integration with Spring Boot Actuator
- Debugging why metrics aren’t being recorded in tests
Quick Start
For unit tests, use SimpleMeterRegistry:
class ChargeServiceTest {
private SimpleMeterRegistry registry;
private ChargeService chargeService;
@BeforeEach
void setUp() {
registry = new SimpleMeterRegistry();
chargeService = new ChargeService(registry);
}
@Test
void testChargeProcessingRecordsMetrics() {
Charge charge = new Charge("SUP123", BigDecimal.valueOf(150.00));
chargeService.processCharge(charge);
Counter counter = registry.get("charge.processed").counter();
assertThat(counter.count()).isEqualTo(1);
}
}
Instructions
Step 1: Choose the Right Registry for Your Test
SimpleMeterRegistry (unit tests, no Spring):
- Use for testing service logic in isolation
- Fast, no Spring context startup
- Meters accessible immediately after recording
- No percentile calculations (use assertions instead)
MeterRegistry with @AutoConfigureMetrics (integration tests):
- Use when testing with Spring context
- Spring auto-configures actual metrics
- Slower but tests real behavior
- Supports percentiles if configured
CompositeMeterRegistry (multi-backend testing):
- Use when testing with multiple backends
- Rarely needed in unit tests
// â Wrong: no registry
class ServiceTest {
private Service service = new Service(); // Needs registry!
}
// â
Unit test: SimpleMeterRegistry
class ServiceTest {
private SimpleMeterRegistry registry = new SimpleMeterRegistry();
private Service service = new Service(registry);
}
// â
Integration test: Spring auto-configures
@SpringBootTest
@AutoConfigureMetrics
class ServiceIntegrationTest {
@Autowired private MeterRegistry registry;
@Autowired private Service service;
}
Step 2: Set Up Test Fixtures
Create test fixtures for common patterns:
// Base test class for metrics testing
class MetricsTestBase {
protected SimpleMeterRegistry registry;
@BeforeEach
void setUp() {
registry = new SimpleMeterRegistry();
}
/**
* Assert that a counter with given name and tags exists and has expected count.
*/
protected void assertCounterValue(String name, long expectedValue, Tag... tags) {
Counter counter = registry.get(name)
.tags(tags)
.counter();
assertThat(counter.count())
.as("Counter %s with tags %s", name, Arrays.asList(tags))
.isEqualTo(expectedValue);
}
/**
* Assert that a timer with given name has recorded expected count.
*/
protected void assertTimerCount(String name, long expectedCount, Tag... tags) {
Timer timer = registry.get(name)
.tags(tags)
.timer();
assertThat(timer.count())
.as("Timer %s count with tags %s", name, Arrays.asList(tags))
.isEqualTo(expectedCount);
}
/**
* Assert that a distribution summary has expected count and total.
*/
protected void assertDistributionSummary(
String name,
long expectedCount,
double expectedTotal,
Tag... tags) {
DistributionSummary summary = registry.get(name)
.tags(tags)
.summary();
assertThat(summary.count())
.as("DistributionSummary %s count", name)
.isEqualTo(expectedCount);
assertThat(summary.totalAmount())
.as("DistributionSummary %s total", name)
.isCloseTo(expectedTotal, within(0.01));
}
/**
* List all metrics for debugging.
*/
protected void printMetrics() {
registry.getMeters().forEach(meter -> {
System.out.println(meter.getId().getName() +
" [" + meter.getId().getTags() + "]: " +
meter.measure());
});
}
}
Step 3: Test Counter Metrics
Test that counters increment correctly:
class ChargeServiceTest extends MetricsTestBase {
private ChargeService chargeService;
@BeforeEach
void setUp() {
super.setUp();
chargeService = new ChargeService(registry);
}
@Test
void testChargeProcessingIncrementsCounter() {
Charge charge = new Charge("SUP123", BigDecimal.valueOf(150.00));
chargeService.processCharge(charge);
assertCounterValue("charge.processed", 1);
}
@Test
void testMultipleChargesIncrementCounter() {
chargeService.processCharge(new Charge("SUP1", BigDecimal.valueOf(100)));
chargeService.processCharge(new Charge("SUP2", BigDecimal.valueOf(200)));
chargeService.processCharge(new Charge("SUP3", BigDecimal.valueOf(300)));
assertCounterValue("charge.processed", 3);
}
@Test
void testChargeRejectionRecordsWithReason() {
chargeService.rejectCharge("validation");
chargeService.rejectCharge("duplicate");
chargeService.rejectCharge("validation");
assertCounterValue("charge.rejected", 2, Tag.of("reason", "validation"));
assertCounterValue("charge.rejected", 1, Tag.of("reason", "duplicate"));
}
}
Step 4: Test Timer Metrics
Test that timers record duration:
class ChargeProcessingTest extends MetricsTestBase {
private ChargeService service;
@BeforeEach
void setUp() {
super.setUp();
service = new ChargeService(registry);
}
@Test
void testChargeApprovalRecordsDuration() {
Charge charge = new Charge("SUP123", BigDecimal.valueOf(150.00));
service.processCharge(charge);
Timer timer = registry.get("charge.approval.duration").timer();
assertThat(timer.count()).isEqualTo(1);
assertThat(timer.totalTime(TimeUnit.MILLISECONDS)).isGreaterThan(0);
}
@Test
void testTimerRecordsAllOperations() {
for (int i = 0; i < 5; i++) {
service.processCharge(new Charge("SUP" + i, BigDecimal.ONE));
}
Timer timer = registry.get("charge.approval.duration").timer();
assertThat(timer.count()).isEqualTo(5);
assertThat(timer.totalTime(TimeUnit.MILLISECONDS))
.isGreaterThan(timer.count() * 1); // At least 1ms per operation
}
@Test
void testTimerTracksMaxDuration() {
service.recordApprovalTime(Duration.ofMillis(10));
service.recordApprovalTime(Duration.ofMillis(100));
service.recordApprovalTime(Duration.ofMillis(50));
Timer timer = registry.get("charge.approval.duration").timer();
assertThat(timer.max(TimeUnit.MILLISECONDS)).isEqualTo(100);
}
}
Step 5: Test Distribution Summary Metrics
Test value distributions:
class ChargeValueMetricsTest extends MetricsTestBase {
private ChargeService service;
@BeforeEach
void setUp() {
super.setUp();
service = new ChargeService(registry);
}
@Test
void testChargeValueDistribution() {
service.recordCharge(new Charge("SUP1", BigDecimal.valueOf(100)));
service.recordCharge(new Charge("SUP2", BigDecimal.valueOf(200)));
service.recordCharge(new Charge("SUP3", BigDecimal.valueOf(300)));
DistributionSummary summary = registry.get("charge.value").summary();
assertThat(summary.count()).isEqualTo(3);
assertThat(summary.totalAmount()).isEqualTo(600.0);
assertThat(summary.mean()).isCloseTo(200.0, within(0.1));
}
@Test
void testChargeValueMax() {
service.recordCharge(new Charge("SUP1", BigDecimal.valueOf(500)));
service.recordCharge(new Charge("SUP2", BigDecimal.valueOf(1000)));
DistributionSummary summary = registry.get("charge.value").summary();
assertThat(summary.max()).isEqualTo(1000.0);
}
}
Step 6: Test Gauge Metrics
Test gauges that fetch values on demand:
class SupplierMetricsTest extends MetricsTestBase {
private SupplierService service;
private SupplierRepository repository;
@BeforeEach
void setUp() {
super.setUp();
repository = mock(SupplierRepository.class);
service = new SupplierService(registry, repository);
}
@Test
void testActiveSupplierCountGauge() {
// Mock repository to return supplier count
when(repository.countActive()).thenReturn(5);
// Gauge is lazy-evaluated on access
Gauge gauge = registry.get("supplier.active.count").gauge();
assertThat(gauge.value()).isEqualTo(5.0);
}
@Test
void testGaugeReflectsChanges() {
when(repository.countActive())
.thenReturn(5) // First call
.thenReturn(8); // Second call
Gauge gauge = registry.get("supplier.active.count").gauge();
assertThat(gauge.value()).isEqualTo(5.0);
// Gauge re-evaluates on each access
assertThat(gauge.value()).isEqualTo(8.0);
}
}
Step 7: Test with Spring Integration Tests
Use @AutoConfigureMetrics for testing with Spring context:
@SpringBootTest
@AutoConfigureMetrics
@AutoConfigureMockMvc
class ChargeControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private MeterRegistry registry;
@BeforeEach
void setUp() {
registry.clear();
}
@Test
void testChargeCreationRecordsMetrics() throws Exception {
mockMvc.perform(post("/charges")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"supplierId": "SUP123",
"amount": 150.00
}
"""))
.andExpect(status().isCreated());
// Verify HTTP metrics
Timer httpTimer = registry.get("http.server.requests")
.tag("uri", "/charges")
.tag("status", "201")
.timer();
assertThat(httpTimer.count()).isEqualTo(1);
// Verify business metrics
Counter chargeCounter = registry.get("charge.processed")
.counter();
assertThat(chargeCounter.count()).isEqualTo(1);
}
@Test
void testHttpMetricsIncludeException() throws Exception {
mockMvc.perform(post("/charges")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"supplierId": null,
"amount": 0
}
"""))
.andExpect(status().isBadRequest());
Timer httpTimer = registry.get("http.server.requests")
.tag("uri", "/charges")
.tag("status", "400")
.timer();
assertThat(httpTimer.count()).isEqualTo(1);
}
}
Step 8: Test Percentiles
Test that percentiles are calculated correctly:
class PercentileTest {
private SimpleMeterRegistry registry;
@BeforeEach
void setUp() {
registry = new SimpleMeterRegistry();
}
@Test
void testTimerPercentiles() {
Timer timer = Timer.builder("test.timer")
.publishPercentiles(0.5, 0.95, 0.99)
.register(registry);
// Record ordered samples
long[] samples = {10, 50, 100, 200, 500, 1000};
for (long sample : samples) {
timer.record(sample, TimeUnit.MILLISECONDS);
}
// Get snapshot with percentiles
Snapshot snapshot = timer.takeSnapshot();
// Verify percentiles (note: exact values depend on implementation)
assertThat(snapshot.percentileValues())
.extracting(ValueAtPercentile::percentile)
.contains(0.5, 0.95, 0.99);
assertThat(snapshot.percentileValues()[0].value(TimeUnit.MILLISECONDS))
.isBetween(50.0, 150.0); // P50 should be around median
}
@Test
void testHistogramBuckets() {
Timer timer = Timer.builder("test.timer")
.serviceLevelObjectives(
Duration.ofMillis(10).toNanos(),
Duration.ofMillis(100).toNanos(),
Duration.ofMillis(500).toNanos()
)
.register(registry);
timer.record(5, TimeUnit.MILLISECONDS);
timer.record(50, TimeUnit.MILLISECONDS);
timer.record(200, TimeUnit.MILLISECONDS);
timer.record(1000, TimeUnit.MILLISECONDS);
List<Measurement> measurements = timer.measure();
// Verify count
long count = measurements.stream()
.filter(m -> m.getStatistic() == Statistic.COUNT)
.mapToLong(m -> (long) m.getValue())
.sum();
assertThat(count).isEqualTo(4);
}
}
Examples
Example 1: Complete Service Test
class InvoiceServiceTest extends MetricsTestBase {
private InvoiceService service;
private InvoiceRepository repository;
@BeforeEach
void setUp() {
super.setUp();
repository = mock(InvoiceRepository.class);
service = new InvoiceService(registry, repository);
}
@Test
void testInvoiceGenerationRecordsMetrics() {
List<Charge> charges = List.of(
new Charge("SUP1", BigDecimal.valueOf(100)),
new Charge("SUP2", BigDecimal.valueOf(200))
);
Invoice invoice = service.generateInvoice(charges);
// Verify counter
assertCounterValue("invoice.generated", 1);
// Verify amount distribution
assertDistributionSummary(
"invoice.amount",
1,
300.0
);
// Verify generation time
Timer generationTimer = registry.get("invoice.generation.duration")
.timer();
assertThat(generationTimer.count()).isEqualTo(1);
}
@Test
void testInvoiceSentRecordsMetric() {
Invoice invoice = service.generateInvoice(List.of());
service.sendInvoice(invoice);
assertCounterValue("invoice.sent", 1, Tag.of("channel", "email"));
}
}
Example 2: Test Metric Cardinality
Ensure tags stay bounded:
class CardinalityTest extends MetricsTestBase {
private ChargeService service;
@BeforeEach
void setUp() {
super.setUp();
service = new ChargeService(registry);
}
@Test
void testChargeRejectionReasonsCategorized() {
// Record many different rejection reasons
service.rejectCharge("validation_error_supplier_id_null");
service.rejectCharge("validation_error_amount_zero");
service.rejectCharge("duplicate_within_24h");
service.rejectCharge("supplier_inactive");
// Should map to bounded categories
Set<Tag> uniqueTags = registry.get("charge.rejected")
.getMeterRegistry()
.getMeters()
.stream()
.filter(m -> m.getId().getName().equals("charge.rejected"))
.flatMap(m -> m.getId().getTags().stream())
.collect(Collectors.toSet());
// Only a few categories, not 4+ unique values
assertThat(uniqueTags)
.extracting(Tag::getValue)
.containsOnly("validation", "duplicate", "supplier_issue");
}
@Test
void testNoHighCardinalityTags() {
// Try to create metrics with unbounded tags
// Should be rejected or normalized by MeterFilter
int initialMeterCount = registry.getMeters().size();
// This would normally create unbounded metrics
// But service should normalize...
// (actual implementation depends on service)
int finalMeterCount = registry.getMeters().size();
assertThat(finalMeterCount)
.isLessThan(initialMeterCount + 100); // Reasonable growth
}
}
Requirements
- Spring Boot 2.1+ with
spring-boot-starter-actuator - Micrometer core library (included with Spring Boot)
- JUnit 5 with AssertJ for assertions
- Mockito for mocking repositories/services
- Java 11+
Anti-Patterns to Avoid
// â Wrong: not clearing registry between tests
@Test
void test1() {
registry.get("counter").counter().increment();
}
@Test
void test2() {
// counter from test1 still exists!
registry.get("counter").counter(); // Wrong count
}
// â
Right: clear registry in @BeforeEach
@BeforeEach
void setUp() {
registry = new SimpleMeterRegistry(); // Fresh registry
}
// â Wrong: ignoring registry failures
Counter counter = registry.get("metric").counter(); // May not exist!
// â
Right: assert meter exists with proper tags
Counter counter = registry.get("metric")
.tag("expected", "value")
.counter();
// â Wrong: testing without proper registry injection
class ServiceTest {
private Service service = new Service(); // Registry is null!
}
// â
Right: inject registry in constructor
class ServiceTest {
private SimpleMeterRegistry registry;
private Service service;
@BeforeEach
void setUp() {
registry = new SimpleMeterRegistry();
service = new Service(registry);
}
}
See Also
- python-micrometer-business-metrics – Creating business metrics
- python-micrometer-cardinality-control – Managing metric cardinality
- python-micrometer-metrics-setup – Initial configuration