Releases: JohnVerheij/MetricsAssertions.TUnit
v0.3.0
[0.3.0] - 2026-06-15: tolerance overloads on the value assertions
Minor release. Adds absolute-tolerance overloads to the value assertions, closing the asymmetry where the histogram aggregates (HasSampleSum / HasSampleAverage) accepted a tolerance but the value assertions compared doubles exactly. Purely additive; the 0.2.0 ApiCompat baseline is preserved.
Added
HasLastValue(double expected, double tolerance)on anInstrumentCaptureasserts the most recent value within an absolute tolerance, the tolerant counterpart ofHasLastValue(double)for gauges fed computed doubles (where0.1 + 0.2 != 0.3exact comparison flakes).HasSamples(IReadOnlyList<double> expected, double tolerance)andHasSamplesInAnyOrder(IReadOnlyList<double> expected, double tolerance)on aMeasurementSetassert the samples within an absolute per-sample tolerance, in order and order-independent respectively. The framework-agnostic core gains the matchingMeasurementSet.SamplesEqual(expected, tolerance)/SamplesEquivalentTo(expected, tolerance)comparison primitives. Every tolerance overload rejects a non-finite or negative tolerance withArgumentOutOfRangeException, so an invalid tolerance fails fast instead of silently matching everything (NaN) or nothing (negative).
Documentation
MeasurementSet.Totalnow documents that the sum is rounded to alongwith banker's rounding (round half to even), so aCounter<double>with a fractional total is evaluated as an integer. For an exact fractional total, compareSumdirectly or useHasSampleSum(expected, tolerance).
v0.2.0
[0.2.0] - 2026-06-12: capture meters created by an IMeterFactory
Minor release. Adds scope-aware capture so a meter created by an IMeterFactory (the standard ASP.NET Core DI metrics path) can be captured by name. Purely additive.
Added
InstrumentCapture.OfName<T>(object? meterScope, string meterName, string instrumentName, TimeProvider?)andMeterCapture.For(string meterName, object? meterScope)capture an instrument on a meter whoseMeter.ScopeequalsmeterScope. A meter created by anIMeterFactorycarries the factory as its scope, so pass that factory to capture it; the existing no-scope overloads match only a meter created directly (new Meter(name)) and silently capture nothing from a factory-created one. The no-scope overloads now delegate to the scope-aware ones with a null scope, so existing behavior is unchanged. The by-referenceMeterCapture.Add(Instrument<T>)now also requires the instrument's meter scope to match the bundle's scope (it already required the meter name to match), so an instrument from a same-named but differently-scoped meter is rejected.
Changed
- Bumped
PackageValidationBaselineVersionfrom0.0.1to0.1.0on both packages so ApiCompat strict-mode validates0.2.0against the most recently published baseline. The new overloads are recorded as additive differences inCompatibilitySuppressions.xml.
v0.1.0
[0.1.0] - 2026-06-07: full assertion surface
The full surface: a queryable MeasurementSet, multi-instrument MeterCapture, meter-introspection
MeterInspector, baseline deltas, and the complete Assert.That assertion vocabulary across all three.
Added
MeasurementSet(core): an immutable, queryable set of captured measurements with counter-style
(Total) and histogram-style (Sum/Min/Max/Average) aggregates, rawValues/All, ordered and
order-insensitive sample comparisons, tag and instrument narrowing (Tagged/ForInstrument), range and
tag predicates, and a deterministicToSnapshotStringprojection.MeterCapture(core): a meter-wide bundle composed from per-instrument captures, built fluently
withFor+Add, queried per instrument or across all viaMeasurements, withRecordObservable
for observable gauges.MeterInspector(core): discovers which instruments a meter publishes
(PublishedInstrumentNames/IsPublished/PublishesAll) via a short-livedMeterListener.InstrumentCapture(core): expanded withOfName/OfObservableconstruction,Total/LastValue,
tag queries (Tagged/HasMeasurementTagged), baseline deltas (Snapshot/Since+MeasurementBaseline),
RecordObservable, andWaitForAsync.MetricsAssertions.TUnit(adapter): the fullAssert.That(...).Has*vocabulary (counter and
up-down-counter totals, measurement counts, emptiness, last value, histogram sum/average/range, exact and
order-insensitive sample sets, and tag-consistency) overInstrumentCapture,MeasurementSet, and
MeterCapture.- Failure diagnostics: every assertion dumps the captured measurements (instrument, value, tags,
timestamp) under the failure, so a mismatch shows what was actually recorded, not only the mismatched
scalar. - Hardening: tolerance arguments are validated (finite, non-negative), inverted range bounds are
rejected, baselines are bound to their originating capture, duplicateMeterCaptureinstrument names
dispose the prior capture, and unknown meter-capture instruments fail as assertions rather than throwing.
Changed
- Breaking:
InstrumentCapture.Measurementsnow returns aMeasurementSetinstead of
IReadOnlyList<CapturedMeasurement>. UseMeasurements.Allfor the underlying list, or the new query
surface directly.
v0.0.1
0.0.1 - 2026-06-07: foundation release
Foundation release. Reserves the package names and ships a real, minimal surface: single-instrument
capture and a first assertion. The full surface lands in 0.1.0.
Added
MetricsAssertions(core):InstrumentCapturecaptures the measurements a referenceable
Instrument<T>records, built on the first-partyMetricCollector<T>testing primitive.
Of<T>(Instrument<T>, TimeProvider?)constructs a capture;Measurementsis the projected
snapshot andCountthe measurement count. Each measurement is aCapturedMeasurement
(instrument name, value projected todouble, tags, timestamp).MetricsAssertions.TUnit(adapter):Assert.That(capture).HasMeasurementCount(n), generated
via TUnit's[GenerateAssertion]source generator.