Skip to content

Commit 4456b83

Browse files
committed
feat: add IsCloseTo and IsWithinPercentOf numeric assertions
Add relative and absolute tolerance assertions for numeric types (double, float, int, long, decimal): - IsCloseTo(expected, tolerance): asserts |actual - expected| <= tolerance - IsWithinPercentOf(expected, percent): asserts |actual - expected| <= |expected * percent / 100| Both assertions follow the existing pattern using [AssertionExtension] for source-generated extension methods. Includes proper handling of NaN, infinity, and edge cases for floating-point types. Closes #4870
1 parent 86249de commit 4456b83

3 files changed

Lines changed: 841 additions & 0 deletions

File tree

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
using TUnit.Assertions.Extensions;
2+
3+
namespace TUnit.Assertions.Tests;
4+
5+
public class NumericToleranceAssertionTests
6+
{
7+
// ==========================================
8+
// IsCloseTo tests - double
9+
// ==========================================
10+
11+
[Test]
12+
public async Task Double_IsCloseTo_Within_Tolerance_Passes()
13+
{
14+
double value = 10.5;
15+
await Assert.That(value).IsCloseTo(10.0, 0.5);
16+
}
17+
18+
[Test]
19+
public async Task Double_IsCloseTo_Exact_Match_Passes()
20+
{
21+
double value = 3.14;
22+
await Assert.That(value).IsCloseTo(3.14, 0.0);
23+
}
24+
25+
[Test]
26+
public async Task Double_IsCloseTo_Outside_Tolerance_Fails()
27+
{
28+
double value = 10.0;
29+
await Assert.ThrowsAsync<AssertionException>(
30+
async () => await Assert.That(value).IsCloseTo(12.0, 1.0));
31+
}
32+
33+
[Test]
34+
public async Task Double_IsCloseTo_NaN_Both_Passes()
35+
{
36+
await Assert.That(double.NaN).IsCloseTo(double.NaN, 0.1);
37+
}
38+
39+
[Test]
40+
public async Task Double_IsCloseTo_NaN_Actual_Fails()
41+
{
42+
await Assert.ThrowsAsync<AssertionException>(
43+
async () => await Assert.That(double.NaN).IsCloseTo(1.0, 0.1));
44+
}
45+
46+
[Test]
47+
public async Task Double_IsCloseTo_Negative_Values_Passes()
48+
{
49+
double value = -5.1;
50+
await Assert.That(value).IsCloseTo(-5.0, 0.2);
51+
}
52+
53+
// ==========================================
54+
// IsCloseTo tests - float
55+
// ==========================================
56+
57+
[Test]
58+
public async Task Float_IsCloseTo_Within_Tolerance_Passes()
59+
{
60+
float value = 10.5f;
61+
await Assert.That(value).IsCloseTo(10.0f, 0.5f);
62+
}
63+
64+
[Test]
65+
public async Task Float_IsCloseTo_Outside_Tolerance_Fails()
66+
{
67+
float value = 10.0f;
68+
await Assert.ThrowsAsync<AssertionException>(
69+
async () => await Assert.That(value).IsCloseTo(12.0f, 1.0f));
70+
}
71+
72+
[Test]
73+
public async Task Float_IsCloseTo_NaN_Both_Passes()
74+
{
75+
await Assert.That(float.NaN).IsCloseTo(float.NaN, 0.1f);
76+
}
77+
78+
// ==========================================
79+
// IsCloseTo tests - int
80+
// ==========================================
81+
82+
[Test]
83+
public async Task Int_IsCloseTo_Within_Tolerance_Passes()
84+
{
85+
int value = 105;
86+
await Assert.That(value).IsCloseTo(100, 5);
87+
}
88+
89+
[Test]
90+
public async Task Int_IsCloseTo_Exact_Match_Passes()
91+
{
92+
int value = 42;
93+
await Assert.That(value).IsCloseTo(42, 0);
94+
}
95+
96+
[Test]
97+
public async Task Int_IsCloseTo_Outside_Tolerance_Fails()
98+
{
99+
int value = 100;
100+
await Assert.ThrowsAsync<AssertionException>(
101+
async () => await Assert.That(value).IsCloseTo(110, 5));
102+
}
103+
104+
// ==========================================
105+
// IsCloseTo tests - long
106+
// ==========================================
107+
108+
[Test]
109+
public async Task Long_IsCloseTo_Within_Tolerance_Passes()
110+
{
111+
long value = 1000000005L;
112+
await Assert.That(value).IsCloseTo(1000000000L, 10L);
113+
}
114+
115+
[Test]
116+
public async Task Long_IsCloseTo_Outside_Tolerance_Fails()
117+
{
118+
long value = 100L;
119+
await Assert.ThrowsAsync<AssertionException>(
120+
async () => await Assert.That(value).IsCloseTo(200L, 50L));
121+
}
122+
123+
// ==========================================
124+
// IsCloseTo tests - decimal
125+
// ==========================================
126+
127+
[Test]
128+
public async Task Decimal_IsCloseTo_Within_Tolerance_Passes()
129+
{
130+
decimal value = 10.05m;
131+
await Assert.That(value).IsCloseTo(10.0m, 0.1m);
132+
}
133+
134+
[Test]
135+
public async Task Decimal_IsCloseTo_Outside_Tolerance_Fails()
136+
{
137+
decimal value = 10.0m;
138+
await Assert.ThrowsAsync<AssertionException>(
139+
async () => await Assert.That(value).IsCloseTo(12.0m, 1.0m));
140+
}
141+
142+
// ==========================================
143+
// IsWithinPercentOf tests - double
144+
// ==========================================
145+
146+
[Test]
147+
public async Task Double_IsWithinPercentOf_Passes()
148+
{
149+
double value = 105.0;
150+
await Assert.That(value).IsWithinPercentOf(100.0, 10.0);
151+
}
152+
153+
[Test]
154+
public async Task Double_IsWithinPercentOf_Exact_Match_Passes()
155+
{
156+
double value = 100.0;
157+
await Assert.That(value).IsWithinPercentOf(100.0, 0.0);
158+
}
159+
160+
[Test]
161+
public async Task Double_IsWithinPercentOf_At_Boundary_Passes()
162+
{
163+
double value = 110.0;
164+
await Assert.That(value).IsWithinPercentOf(100.0, 10.0);
165+
}
166+
167+
[Test]
168+
public async Task Double_IsWithinPercentOf_Outside_Fails()
169+
{
170+
double value = 120.0;
171+
await Assert.ThrowsAsync<AssertionException>(
172+
async () => await Assert.That(value).IsWithinPercentOf(100.0, 10.0));
173+
}
174+
175+
[Test]
176+
public async Task Double_IsWithinPercentOf_Negative_Expected_Passes()
177+
{
178+
double value = -95.0;
179+
await Assert.That(value).IsWithinPercentOf(-100.0, 10.0);
180+
}
181+
182+
[Test]
183+
public async Task Double_IsWithinPercentOf_NaN_Both_Passes()
184+
{
185+
await Assert.That(double.NaN).IsWithinPercentOf(double.NaN, 10.0);
186+
}
187+
188+
// ==========================================
189+
// IsWithinPercentOf tests - float
190+
// ==========================================
191+
192+
[Test]
193+
public async Task Float_IsWithinPercentOf_Passes()
194+
{
195+
float value = 105.0f;
196+
await Assert.That(value).IsWithinPercentOf(100.0f, 10.0f);
197+
}
198+
199+
[Test]
200+
public async Task Float_IsWithinPercentOf_Outside_Fails()
201+
{
202+
float value = 120.0f;
203+
await Assert.ThrowsAsync<AssertionException>(
204+
async () => await Assert.That(value).IsWithinPercentOf(100.0f, 10.0f));
205+
}
206+
207+
// ==========================================
208+
// IsWithinPercentOf tests - int
209+
// ==========================================
210+
211+
[Test]
212+
public async Task Int_IsWithinPercentOf_Passes()
213+
{
214+
int value = 105;
215+
await Assert.That(value).IsWithinPercentOf(100, 10.0);
216+
}
217+
218+
[Test]
219+
public async Task Int_IsWithinPercentOf_Outside_Fails()
220+
{
221+
int value = 120;
222+
await Assert.ThrowsAsync<AssertionException>(
223+
async () => await Assert.That(value).IsWithinPercentOf(100, 10.0));
224+
}
225+
226+
// ==========================================
227+
// IsWithinPercentOf tests - long
228+
// ==========================================
229+
230+
[Test]
231+
public async Task Long_IsWithinPercentOf_Passes()
232+
{
233+
long value = 1050L;
234+
await Assert.That(value).IsWithinPercentOf(1000L, 10.0);
235+
}
236+
237+
[Test]
238+
public async Task Long_IsWithinPercentOf_Outside_Fails()
239+
{
240+
long value = 1200L;
241+
await Assert.ThrowsAsync<AssertionException>(
242+
async () => await Assert.That(value).IsWithinPercentOf(1000L, 10.0));
243+
}
244+
245+
// ==========================================
246+
// IsWithinPercentOf tests - decimal
247+
// ==========================================
248+
249+
[Test]
250+
public async Task Decimal_IsWithinPercentOf_Passes()
251+
{
252+
decimal value = 105.0m;
253+
await Assert.That(value).IsWithinPercentOf(100.0m, 10.0m);
254+
}
255+
256+
[Test]
257+
public async Task Decimal_IsWithinPercentOf_Outside_Fails()
258+
{
259+
decimal value = 120.0m;
260+
await Assert.ThrowsAsync<AssertionException>(
261+
async () => await Assert.That(value).IsWithinPercentOf(100.0m, 10.0m));
262+
}
263+
264+
// ==========================================
265+
// Edge cases
266+
// ==========================================
267+
268+
[Test]
269+
public async Task Double_IsCloseTo_Zero_Expected_Passes()
270+
{
271+
double value = 0.001;
272+
await Assert.That(value).IsCloseTo(0.0, 0.01);
273+
}
274+
275+
[Test]
276+
public async Task Int_IsWithinPercentOf_Zero_Expected_Fails()
277+
{
278+
// 10% of 0 is 0, so only exact match passes
279+
int value = 1;
280+
await Assert.ThrowsAsync<AssertionException>(
281+
async () => await Assert.That(value).IsWithinPercentOf(0, 10.0));
282+
}
283+
284+
[Test]
285+
public async Task Int_IsWithinPercentOf_Zero_Expected_Zero_Actual_Passes()
286+
{
287+
// 0 is within any percent of 0
288+
int value = 0;
289+
await Assert.That(value).IsWithinPercentOf(0, 10.0);
290+
}
291+
}

0 commit comments

Comments
 (0)