@@ -40,7 +40,7 @@ This document provides specific guidelines for writing tests in the fabric-x-com
4040``` go
4141func TestMyFunction (t *testing .T ) {
4242 t.Parallel ()
43-
43+
4444 t.Run (" success cases" , func (t *testing.T ) { // ❌ Unnecessary nesting
4545 t.Parallel ()
4646 for _ , tc := range []struct {...}{...} {
@@ -49,7 +49,7 @@ func TestMyFunction(t *testing.T) {
4949 })
5050 }
5151 })
52-
52+
5353 t.Run (" failure cases" , func (t *testing.T ) { // ❌ Unnecessary nesting
5454 t.Parallel ()
5555 for _ , tc := range []struct {...}{...} {
@@ -65,7 +65,7 @@ func TestMyFunction(t *testing.T) {
6565``` go
6666func TestMyFunction (t *testing .T ) {
6767 t.Parallel ()
68-
68+
6969 // Success cases
7070 for _ , tc := range []struct {
7171 name string
@@ -89,7 +89,7 @@ func TestMyFunction(t *testing.T) {
8989 require.Equal (t, tc.expected , result)
9090 })
9191 }
92-
92+
9393 // Failure cases
9494 for _ , tc := range []struct {
9595 name string
@@ -206,7 +206,7 @@ require.PanicsWithValue(t, "exact error message", func() {
206206``` go
207207func TestBucketConfig_Buckets (t *testing .T ) {
208208 t.Parallel ()
209-
209+
210210 // Success cases
211211 for _ , tc := range []struct {
212212 name string
@@ -236,7 +236,7 @@ func TestBucketConfig_Buckets(t *testing.T) {
236236 require.Equal (t, tc.expected , result)
237237 })
238238 }
239-
239+
240240 // Failure cases
241241 for _ , tc := range []struct {
242242 name string
@@ -260,6 +260,271 @@ func TestBucketConfig_Buckets(t *testing.T) {
260260}
261261```
262262
263+ ## Metrics Testing
264+
265+ The project provides specialized helper functions in [ ` utils/test/metrics.go ` ] ( ../../utils/test/metrics.go ) for testing Prometheus metrics. Use these helpers consistently across all tests.
266+
267+ ### Available Metrics Testing Functions
268+
269+ #### Direct Metric Value Assertions
270+
271+ ✅ ** CORRECT** - Use [ ` test.RequireIntMetricValue() ` ] ( ../../utils/test/metrics.go:99 ) for immediate assertions:
272+ ``` go
273+ // Assert metric equals expected value immediately
274+ test.RequireIntMetricValue (t, 5 , myMetric)
275+ test.RequireIntMetricValue (t, 0 , myMetric.WithLabelValues (" label" ))
276+
277+ // Verify initial state
278+ test.RequireIntMetricValue (t, 0 , metrics.pendingRequests )
279+ ```
280+
281+ #### Eventual Metric Value Assertions
282+
283+ ✅ ** CORRECT** - Use [ ` test.EventuallyIntMetric() ` ] ( ../../utils/test/metrics.go:105 ) when metrics update asynchronously:
284+ ``` go
285+ // Wait for metric to reach expected value
286+ test.EventuallyIntMetric (
287+ t,
288+ expectedValue,
289+ myMetric,
290+ 5 *time.Second , // waitFor duration
291+ 100 *time.Millisecond , // tick interval
292+ )
293+
294+ // With optional message
295+ test.EventuallyIntMetric (
296+ t,
297+ 10 ,
298+ metrics.transactionsSentTotal ,
299+ 5 *time.Second ,
300+ 10 *time.Millisecond ,
301+ " transactions should be sent" ,
302+ )
303+ ```
304+
305+ #### Getting Metric Values
306+
307+ Use [ ` test.GetIntMetricValue() ` ] ( ../../utils/test/metrics.go:92 ) or [ ` test.GetMetricValue() ` ] ( ../../utils/test/metrics.go:69 ) when you need the value for custom assertions:
308+
309+ ``` go
310+ // Get integer metric value (rounded)
311+ preValue := test.GetIntMetricValue (t, metrics.transactionReceivedTotal )
312+
313+ // Perform operations...
314+
315+ // Assert using the captured value
316+ test.EventuallyIntMetric (t, preValue+10 , metrics.transactionReceivedTotal ,
317+ 5 *time.Second , 100 *time.Millisecond )
318+
319+ // Get float metric value (for histograms, summaries)
320+ latency := test.GetMetricValue (t, metrics.blockMappingInRelaySeconds )
321+ require.Greater (t, latency, float64 (0 ))
322+
323+ // Use with require.EventuallyWithT for complex conditions
324+ require.EventuallyWithT (t, func (ct *assert.CollectT ) {
325+ require.Equal (ct, 0 , test.GetIntMetricValue (t, metrics.queueSize ))
326+ require.Equal (ct, 5 , test.GetIntMetricValue (t, metrics.activeWorkers ))
327+ }, 3 *time.Second , 500 *time.Millisecond )
328+ ```
329+
330+ #### HTTP Metrics Endpoint Testing
331+
332+ For integration tests that check metrics via HTTP:
333+
334+ ``` go
335+ // Check that specific metrics exist in the output
336+ test.CheckMetrics (t, metricsURL, tlsConfig,
337+ " loadgen_block_sent_total" ,
338+ " loadgen_transaction_committed_total" ,
339+ )
340+
341+ // Get specific metric value from HTTP endpoint
342+ count := test.GetMetricValueFromURL (t, metricsURL, " metric_name" , tlsConfig)
343+ require.Greater (t, count, 500 )
344+ ```
345+
346+ ### Metrics Testing Best Practices
347+
348+ 1 . ** Use Appropriate Helper** : Choose the right helper based on timing requirements:
349+ - [ ` test.RequireIntMetricValue() ` ] ( ../../utils/test/metrics.go:99 ) for synchronous operations where the metric should already have the expected value
350+ - [ ` test.EventuallyIntMetric() ` ] ( ../../utils/test/metrics.go:105 ) for asynchronous operations where the metric will reach the expected value after some time
351+ - [ ` test.GetIntMetricValue() ` ] ( ../../utils/test/metrics.go:92 ) for capturing baseline values or when using with ` require.Eventually() ` for complex conditions
352+ - [ ` test.GetMetricValue() ` ] ( ../../utils/test/metrics.go:69 ) for float metrics (histograms, summaries) or when you need the raw float value
353+
354+ 2 . ** Capture Baseline Values** : When testing incremental changes, capture the metric value before the operation:
355+ ``` go
356+ preValue := test.GetIntMetricValue (t, metrics.transactionReceivedTotal )
357+
358+ // perform operation that adds 10 transactions
359+ sendTransactions (10 )
360+
361+ // Verify the increment
362+ test.EventuallyIntMetric (t, preValue+10 , metrics.transactionReceivedTotal ,
363+ 5 *time.Second , 100 *time.Millisecond )
364+ ```
365+
366+ 3 . ** Test Labeled Metrics** : Use [ ` WithLabelValues() ` ] ( https://pkg.go.dev/github.com/prometheus/client_golang/prometheus#CounterVec.WithLabelValues ) to test specific label combinations:
367+ ``` go
368+ test.RequireIntMetricValue (t, 10 ,
369+ metrics.transactionCommittedTotal .WithLabelValues (" COMMITTED" ))
370+ test.RequireIntMetricValue (t, 2 ,
371+ metrics.transactionCommittedTotal .WithLabelValues (" MALFORMED" ))
372+ ```
373+
374+ 4 . ** Use Appropriate Wait Times** : For [ ` test.EventuallyIntMetric() ` ] ( ../../utils/test/metrics.go:105 ) :
375+ - Unit tests: 1-5 seconds wait, 10-100ms tick
376+ - Integration tests: 5-60 seconds wait, 100ms-1s tick
377+ - Examples from codebase:
378+ - Fast operations: ` 5*time.Second, 10*time.Millisecond `
379+ - Normal operations: ` 5*time.Second, 100*time.Millisecond `
380+ - Slow operations: ` 2*time.Second, 200*time.Millisecond `
381+
382+ 5 . ** Test Initial State** : Always verify metrics start at expected initial values:
383+ ``` go
384+ test.RequireIntMetricValue (t, 0 , metrics.pendingRequests )
385+ test.RequireIntMetricValue (t, 0 , metrics.activeStreams )
386+ ```
387+
388+ 6 . ** Verify Metric Decrements** : When testing gauges that can decrease:
389+ ``` go
390+ test.RequireIntMetricValue (t, 5 , metrics.queueSize )
391+ // process items
392+ test.EventuallyIntMetric (t, 0 , metrics.queueSize , 2 *time.Second , 100 *time.Millisecond )
393+ ```
394+
395+ 7 . ** Use ` require.Never() ` for Bounds Testing** : When verifying metrics don't exceed limits:
396+ ``` go
397+ require.Never (t, func () bool {
398+ return test.GetIntMetricValue (t, metrics.counter ) > maxExpected
399+ }, 3 *time.Second , 1 *time.Second )
400+ ```
401+
402+ 8 . ** Use ` require.EventuallyWithT() ` for Multi-Metric Conditions** : When checking multiple metrics, always use ` require.EventuallyWithT() ` instead of ` require.Eventually() ` to ensure clear visibility of which metric failed:
403+ ``` go
404+ // ✅ CORRECT - Shows which metric failed
405+ require.EventuallyWithT (t, func (ct *assert.CollectT ) {
406+ require.Equal (ct, 1 , test.GetIntMetricValue (t, metrics.inputQueue ))
407+ require.Equal (ct, 1 , test.GetIntMetricValue (t, metrics.outputQueue ))
408+ require.Equal (ct, 0 , test.GetIntMetricValue (t, metrics.processingQueue ))
409+ }, 3 *time.Second , 500 *time.Millisecond )
410+
411+ // ❌ INCORRECT - Doesn't show which condition failed
412+ require.Eventually (t, func () bool {
413+ return test.GetIntMetricValue (t, metrics.inputQueue ) == 1 &&
414+ test.GetIntMetricValue (t, metrics.outputQueue ) == 1 &&
415+ test.GetIntMetricValue (t, metrics.processingQueue ) == 0
416+ }, 3 *time.Second , 500 *time.Millisecond )
417+ ```
418+
419+ 9 . ** Use ` require.EventuallyWithT() ` for Single Metrics with Context** : When you need detailed assertion messages for a single metric:
420+ ``` go
421+ require.EventuallyWithT (t, func (ct *assert.CollectT ) {
422+ actual := test.GetIntMetricValue (t, metrics.counter )
423+ require.Equal (ct, expected, actual, " counter should reach expected value" )
424+ }, 5 *time.Second , 100 *time.Millisecond )
425+ ```
426+
427+ 10 . ** Test Timing Metrics** : For histogram and summary metrics, verify they are positive:
428+ ``` go
429+ latency := test.GetMetricValue (t, metrics.requestDurationSeconds )
430+ require.Greater (t, latency, float64 (0 ))
431+ ```
432+
433+ ### Common Patterns
434+
435+ #### Pattern 1 : Incremental Counter Testing
436+ ` ` ` go
437+ // Capture baseline
438+ preValue := test.GetIntMetricValue(t, metrics.transactionReceivedTotal)
439+
440+ // Perform operation
441+ sendTransactions(5)
442+
443+ // Verify increment
444+ test.EventuallyIntMetric(t, preValue+5, metrics.transactionReceivedTotal,
445+ 5*time.Second, 100*time.Millisecond)
446+ ` ` `
447+
448+ #### Pattern 2 : Multiple Labeled Metrics
449+ ` ` ` go
450+ test.RequireIntMetricValue(t, 30, metrics.notifierPendingTxIDs)
451+ test.RequireIntMetricValue(t, 6, metrics.notifierUniquePendingTxIDs)
452+ test.RequireIntMetricValue(t, 10, metrics.notifierTxIDsStatusDeliveries)
453+ test.RequireIntMetricValue(t, 0, metrics.notifierTxIDsTimeoutDeliveries)
454+ ` ` `
455+
456+ #### Pattern 3 : Queue Size Monitoring
457+ ` ` ` go
458+ // Verify queue fills up
459+ test.EventuallyIntMetric(t, 3, metrics.waitingTransactionsQueueSize,
460+ 5*time.Second, 10*time.Millisecond)
461+
462+ // Process items
463+ processQueue()
464+
465+ // Verify queue empties
466+ test.EventuallyIntMetric(t, 0, metrics.waitingTransactionsQueueSize,
467+ 5*time.Second, 10*time.Millisecond)
468+ ` ` `
469+
470+ #### Pattern 4 : Complex Multi -Metric Conditions
471+ ` ` ` go
472+ // Use EventuallyWithT for clear failure messages
473+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
474+ require.Equal(ct, 10, test.GetIntMetricValue(t, metrics.gdgTxProcessedTotal))
475+ require.Equal(ct, 8, test.GetIntMetricValue(t, metrics.gdgValidatedTxProcessedTotal))
476+ }, 2*time.Second, 200*time.Millisecond)
477+ ` ` `
478+
479+ #### Pattern 5 : Integration Test HTTP Metrics
480+ ` ` ` go
481+ require.EventuallyWithT(t, func(ct *assert.CollectT) {
482+ count := test.GetMetricValueFromURL(
483+ t, metricsURL, "loadgen_transaction_committed_total", tlsConfig,
484+ )
485+ require.Greater(ct, count, 500)
486+ }, 300*time.Second, 1*time.Second)
487+ ` ` `
488+
489+ ### Example : Complete Metrics Test
490+
491+ ` ` ` go
492+ func TestRelayMetrics(t *testing.T) {
493+ t.Parallel()
494+
495+ env := setupTestEnvironment(t)
496+ m := env.metrics
497+
498+ // Verify initial state
499+ test.RequireIntMetricValue(t, 0, m.transactionsSentTotal)
500+ test.RequireIntMetricValue(t, 0, m.waitingTransactionsQueueSize)
501+
502+ // Submit transactions
503+ txCount := 3
504+ submitTransactions(txCount)
505+
506+ // Verify metrics update asynchronously
507+ test.EventuallyIntMetric(t, txCount, m.transactionsSentTotal,
508+ 5*time.Second, 10*time.Millisecond)
509+ test.EventuallyIntMetric(t, txCount, m.waitingTransactionsQueueSize,
510+ 5*time.Second, 10*time.Millisecond)
511+
512+ // Process transactions
513+ processTransactions()
514+
515+ // Verify labeled metrics
516+ test.RequireIntMetricValue(t, txCount,
517+ m.transactionsStatusReceivedTotal.WithLabelValues("COMMITTED"))
518+
519+ // Verify queue empties
520+ test.EventuallyIntMetric(t, 0, m.waitingTransactionsQueueSize,
521+ 5*time.Second, 10*time.Millisecond)
522+
523+ // Verify timing metrics are positive
524+ require.Greater(t, test.GetMetricValue(t, m.processingSeconds), float64(0))
525+ }
526+ ` ` `
527+
263528## Summary
264529
265530- **No nested test groups** - Keep table-driven tests flat
@@ -269,3 +534,4 @@ func TestBucketConfig_Buckets(t *testing.T) {
269534- **Descriptive test names**
270535- **Helper functions at the end with ` t.Helper()` **
271536- **Never use ` panic()` in tests**
537+ - **Use metrics testing helpers** - [` test.RequireIntMetricValue()` ](../../utils/test/metrics.go :99 ), [` test.EventuallyIntMetric()` ](../../utils/test/metrics.go :105 ), [` test.GetIntMetricValue()` ](../../utils/test/metrics.go :92 )
0 commit comments