MathAssertions.TUnit 0.3.0

Prefix Reserved
dotnet add package MathAssertions.TUnit --version 0.3.0
                    
NuGet\Install-Package MathAssertions.TUnit -Version 0.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MathAssertions.TUnit" Version="0.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MathAssertions.TUnit" Version="0.3.0" />
                    
Directory.Packages.props
<PackageReference Include="MathAssertions.TUnit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MathAssertions.TUnit --version 0.3.0
                    
#r "nuget: MathAssertions.TUnit, 0.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MathAssertions.TUnit@0.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MathAssertions.TUnit&version=0.3.0
                    
Install as a Cake Addin
#tool nuget:?package=MathAssertions.TUnit&version=0.3.0
                    
Install as a Cake Tool

MathAssertions.TUnit

NuGet Downloads License: MIT .NET

Scope: Test projects only. Not intended for production code.

TUnit-native math assertion library for .NET. Covers tolerance comparisons, sequences, statistics, linear algebra, number theory, and 3D geometry. NaN-aware, infinity-aware, AOT-compatible, no runtime reflection in the assertion path.

Full documentation, "Why component-wise rather than Euclidean", cookbook, design notes, and roadmap: github.com/JohnVerheij/MathAssertions.TUnit

Status: v0.3.0 (pose renderer)

The mathematical-assertion surface is exposed as fluent extensions over TUnit's Assert.That(value).Method(...). ~85 fluent entry points across twelve adapter classes covering scalar tolerance comparisons, the System.Numerics compound types (vector / quaternion / matrix / plane / complex), double[] / float[] element-wise comparison with null-array guards, sequence properties, descriptive statistics, linear-algebra invariants, integer number theory, and a complete 3D-geometry primitive surface (containment / closest-point distance / intersection / pointcloud aggregates). v0.2.0 adds rich per-component / per-cell delta rendering to every compound IsApproximatelyEqualTo failure message plus HasAxisAngleApproximately on Quaternion. v0.3.0 adds the MathAssertions.Render namespace with PoseRenderer for turning a position / orientation pose into deterministic, snapshot-friendly text.

Install

dotnet add package MathAssertions.TUnit

MathAssertions (the framework-agnostic core) comes transitively. Requirements: TUnit 1.43.11 or later, .NET 10.

The source-generated entry point (IsApproximatelyEqualTo on Vector3) auto-imports via TUnit.Assertions.Extensions. The only additional using you typically need is System.Numerics for the value type itself. If you call MathTolerance.IsApproximatelyEqual directly from a [GenerateAssertion] extension on a private domain type, add MathAssertions to your GlobalUsings.cs:

global using MathAssertions;

Quick start

using System.Numerics;

[Test]
public async Task ComputedPositionIsApproximatelyAtTarget(CancellationToken ct)
{
    Vector3 target = new(0.300f, 0.150f, 0.450f);
    Vector3 actual = SolveTrajectory(input);

    await Assert.That(actual).IsApproximatelyEqualTo(target, tolerance: 1e-3);
}

Entry points

The fluent surface, organized by adapter class:

Class Coverage
ScalarAssertions IsApproximatelyEqualTo, IsCloseInUlpsTo, IsRelativelyAndAbsolutelyCloseTo, IsNonNegativeFinite, IsProbability, IsPercentage, HasRoundtripIdentity for double/float.
VectorAssertions IsApproximatelyEqualTo for Vector2/Vector3/Vector4; Vector3.HasMagnitudeApproximately, IsNormalized.
QuaternionAssertions IsApproximatelyEqualTo, IsRotationallyEquivalentTo (SO(3) double-cover), HasAxisAngleApproximately (axis-angle form; v0.2.0+), IsIdentity, IsNormalized.
MatrixAssertions Matrix4x4.IsApproximatelyEqualTo plus the full invariant surface: IsSymmetric, IsOrthogonal, IsIdentity, HasDeterminantApproximately, HasTraceApproximately, IsInvertible.
PlaneAssertions IsApproximatelyEqualTo, IsGeometricallyEquivalentTo (sign-flip equivalence).
ComplexAssertions IsApproximatelyEqualTo.
ArrayAssertions double[] / float[] element-wise IsApproximatelyEqualTo with ArgumentNullException on null arrays.
SequencesAssertions Monotonicity, IsBounded, IsArithmeticProgression, IsGeometricProgression, ConvergesTo, IsCauchyConvergent, generic HasLength, HasMinLength over T[].
StatisticsAssertions HasMean/Variance/StdDev/Sum/Median/PercentileApproximately, IsWithinSigmasOfMean, AreAllWithinSigmasOfMean.
LinearAlgebraAssertions Vector3.IsOrthogonalTo, IsParallelTo; Vector3[].AreLinearlyIndependent.
NumberTheoryAssertions long-integer predicates: IsDivisibleBy, IsPrime, IsCoprimeWith, IsPowerOf, IsPerfectSquare, IsCongruentTo.
Geometry3DAssertions Triangle / point-set property predicates, containment (point/box/sphere/OBB/convex hull), predicate-style HasDistanceFromPlane/Segment/Triangle, ray-plane/sphere/triangle/AABB intersection, pointcloud aggregates.

IsFinite() for double/float is provided by TUnit's built-in DoubleAssertionExtensions/SingleAssertionExtensions; this package does not duplicate it.

Cookbook

Pattern: comparing quaternions

IsApproximatelyEqualTo is component-wise. Two unit quaternions q and -q represent the same physical rotation but fail component-wise comparison. If the production code may emit either sign of a unit quaternion (calibration outputs, slerp interpolation, normalization that picks a sign), use IsRotationallyEquivalentTo:

[Test]
public async Task RotationOutputMatchesExpected(CancellationToken ct)
{
    Quaternion expected = Quaternion.CreateFromAxisAngle(Vector3.UnitY, MathF.PI / 4);
    Quaternion actual = SolveOrientation(input);  // may emit ±expected

    // Component-wise: fails if SolveOrientation returned -expected
    // await Assert.That(actual).IsApproximatelyEqualTo(expected, tolerance: 1e-6);

    // Rotational: treats q and -q as the same rotation
    await Assert.That(actual).IsRotationallyEquivalentTo(expected, tolerance: 1e-6);
}

Use IsApproximatelyEqualTo when component identity matters (serialization roundtrips, exact storage formats). Use IsRotationallyEquivalentTo when geometric meaning matters (physical orientations, rotations applied to vectors).

Pattern: comparing planes

IsApproximatelyEqualTo is component-wise on the plane equation (n.X, n.Y, n.Z, d). The same geometric plane has two valid representations: (n, d) and (-n, -d). If the production code constructs planes via different paths (three-point, normal-and-distance, normal-and-point), the sign of the normal may differ between expected and actual without changing the plane. Use IsGeometricallyEquivalentTo:

[Test]
public async Task GroundPlaneMatchesExpected(CancellationToken ct)
{
    Plane expected = new(Vector3.UnitY, -1.0f);  // y = 1
    Plane actual = ComputeGroundPlane(input);    // could be (UnitY, -1) or (-UnitY, 1)

    await Assert.That(actual).IsGeometricallyEquivalentTo(expected, tolerance: 1e-6);
}

Use IsApproximatelyEqualTo when normal direction is observable in the consumer (winding-aware shading, half-space convention). Use IsGeometricallyEquivalentTo when only the plane's geometry is observable.

Verifying invertible transformations: HasRoundtripIdentity

When a transformation is invertible, the framework-agnostic MathTolerance.HasRoundtripIdentity(double input, Func<double,double> forward, Func<double,double> backward, double tolerance) checks that backward(forward(input)) lands back at input within tolerance. The primitive is double-only on purpose; consumers wrap their own typed inputs.

// Sin / Asin compose back to the input.
await Assert.That(MathTolerance.HasRoundtripIdentity(
    0.42, Math.Sin, Math.Asin, tolerance: 1e-12)).IsTrue();

// Degree / radian conversion.
await Assert.That(MathTolerance.HasRoundtripIdentity(
    45.0,
    d => d * Math.PI / 180.0,
    r => r * 180.0 / Math.PI,
    tolerance: 1e-12)).IsTrue();

For typed inputs (vector / quaternion / serializer pair), compose [GenerateAssertion] on your own type rather than chaining HasRoundtripIdentity. The primitive is intentionally double-only so the family does not lock in a particular cross-type roundtrip surface.

Pattern: detecting an unpopulated zero quaternion

A zero-valued quaternion (all four components zero, length zero) is a valid in-memory state but not a valid rotation. Common cases: a protobuf default for an unpopulated Quaternion field, an in-construction calibration output that has not yet been written, an explicit "no rotation set" sentinel.

The BCL exposes Quaternion.Zero. The existing component-wise comparison handles the assertion without a specialized API:

[Test]
public async Task UnpopulatedRotationIsZeroSentinel(CancellationToken ct)
{
    Quaternion actual = ReadRotationFromMessage(message);

    await Assert.That(actual).IsApproximatelyEqualTo(Quaternion.Zero, tolerance: 1e-9);
}

This reads as "the quaternion is approximately the zero quaternion" and fails for any rotation including the identity rotation Quaternion.Identity (which has W = 1).

NaN and infinity semantics

The fluent extensions match the underlying MathTolerance semantics, which match TUnit's IsCloseTo primitive:

Both Comparison Result
NaN NaN true (under any tolerance)
One NaN, other not (anything) false
Same-sign infinity (anything) true
Opposite-sign infinity (anything) false
Finite Math.Abs(a - b) <= tolerance as expected
Tolerance is NaN or negative (anything) ArgumentOutOfRangeException at call time

For compound types (Vector2/Vector3/Vector4, Quaternion, Matrix4x4, Plane, Complex), every component pair is evaluated against this table; the assertion passes iff every pair passes.

Extending to your own types

Add tolerance assertions for your own domain types via a one-line [GenerateAssertion] extension calling MathTolerance.IsApproximatelyEqual on each component. The package never sees your private types.

using MathAssertions;
using TUnit.Assertions.Attributes;

file static class PositionAssertions
{
    [GenerateAssertion(
        ExpectationMessage = "to be approximately equal to {expected} within tolerance {tolerance}",
        InlineMethodBody = true)]
    public static bool IsApproximatelyEqualTo(this MyPosition value, MyPosition expected, double tolerance)
        => MathTolerance.IsApproximatelyEqual(value.AsVector3(), expected.AsVector3(), tolerance);
}

Failure diagnostics

On a failed assertion, the exception message renders the actual Vector3 against the expected and the supplied tolerance:

Expected:
  to be approximately equal to <1, 2, 99> component-wise within tolerance 0.001

Actual:
  <1, 2, 3>

Full failure-diagnostics examples, design notes, stability intent, and roadmap on GitHub.

Family

Part of an assertion family for TUnit:

License

MIT. Copyright (c) 2026 John Verheij.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.3.0 159 5/14/2026
0.2.0 194 5/12/2026
0.1.0 143 5/9/2026
0.0.1 123 5/8/2026

See CHANGELOG.md