Skip to content

Commit 5160ae0

Browse files
authored
feat(expect): add asymmetric matcher expect.closeTo (#12243)
1 parent 33b2cc0 commit 5160ae0

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### Features
44

5+
- `[expect]` Add asymmetric matcher `expect.closeTo` ([#12243](https://github.com/facebook/jest/pull/12243))
56
- `[jest-mock]` Added `mockFn.mock.lastCall` to retrieve last argument ([#12285](https://github.com/facebook/jest/pull/12285))
67

78
### Fixes

docs/ExpectAPI.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,26 @@ test('doAsync calls both callbacks', () => {
432432

433433
The `expect.assertions(2)` call ensures that both callbacks actually get called.
434434

435+
### `expect.closeTo(number, numDigits?)`
436+
437+
`expect.closeTo(number, numDigits?)` is useful when comparing floating point numbers in object properties or array item. If you need to compare a number, please use `.toBeCloseTo` instead.
438+
439+
The optional `numDigits` argument limits the number of digits to check **after** the decimal point. For the default value `2`, the test criterion is `Math.abs(expected - received) < 0.005 (that is, 10 ** -2 / 2)`.
440+
441+
For example, this test passes with a precision of 5 digits:
442+
443+
```js
444+
test('compare float in object properties', () => {
445+
expect({
446+
title: '0.1 + 0.2',
447+
sum: 0.1 + 0.2,
448+
}).toEqual({
449+
title: '0.1 + 0.2',
450+
sum: expect.closeTo(0.3, 5),
451+
});
452+
});
453+
```
454+
435455
### `expect.hasAssertions()`
436456

437457
`expect.hasAssertions()` verifies that at least one assertion is called during a test. This is often useful when testing asynchronous code, in order to make sure that assertions in a callback actually got called.

packages/expect/src/__tests__/asymmetricMatchers.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
anything,
1313
arrayContaining,
1414
arrayNotContaining,
15+
closeTo,
16+
notCloseTo,
1517
objectContaining,
1618
objectNotContaining,
1719
stringContaining,
@@ -377,3 +379,105 @@ test('StringNotMatching throws if expected value is neither string nor regexp',
377379
test('StringNotMatching returns true if received value is not string', () => {
378380
jestExpect(stringNotMatching('en').asymmetricMatch(1)).toBe(true);
379381
});
382+
383+
describe('closeTo', () => {
384+
[
385+
[0, 0],
386+
[0, 0.001],
387+
[1.23, 1.229],
388+
[1.23, 1.226],
389+
[1.23, 1.225],
390+
[1.23, 1.234],
391+
[Infinity, Infinity],
392+
[-Infinity, -Infinity],
393+
].forEach(([expected, received]) => {
394+
test(`${expected} closeTo ${received} return true`, () => {
395+
jestExpect(closeTo(expected).asymmetricMatch(received)).toBe(true);
396+
});
397+
test(`${expected} notCloseTo ${received} return false`, () => {
398+
jestExpect(notCloseTo(expected).asymmetricMatch(received)).toBe(false);
399+
});
400+
});
401+
402+
[
403+
[0, 0.01],
404+
[1, 1.23],
405+
[1.23, 1.2249999],
406+
[Infinity, -Infinity],
407+
[Infinity, 1.23],
408+
[-Infinity, -1.23],
409+
].forEach(([expected, received]) => {
410+
test(`${expected} closeTo ${received} return false`, () => {
411+
jestExpect(closeTo(expected).asymmetricMatch(received)).toBe(false);
412+
});
413+
test(`${expected} notCloseTo ${received} return true`, () => {
414+
jestExpect(notCloseTo(expected).asymmetricMatch(received)).toBe(true);
415+
});
416+
});
417+
418+
[
419+
[0, 0.1, 0],
420+
[0, 0.0001, 3],
421+
[0, 0.000004, 5],
422+
[2.0000002, 2, 5],
423+
].forEach(([expected, received, precision]) => {
424+
test(`${expected} closeTo ${received} with precision ${precision} return true`, () => {
425+
jestExpect(closeTo(expected, precision).asymmetricMatch(received)).toBe(
426+
true,
427+
);
428+
});
429+
test(`${expected} notCloseTo ${received} with precision ${precision} return false`, () => {
430+
jestExpect(
431+
notCloseTo(expected, precision).asymmetricMatch(received),
432+
).toBe(false);
433+
});
434+
});
435+
436+
[
437+
[3.141592e-7, 3e-7, 8],
438+
[56789, 51234, -4],
439+
].forEach(([expected, received, precision]) => {
440+
test(`${expected} closeTo ${received} with precision ${precision} return false`, () => {
441+
jestExpect(closeTo(expected, precision).asymmetricMatch(received)).toBe(
442+
false,
443+
);
444+
});
445+
test(`${expected} notCloseTo ${received} with precision ${precision} return true`, () => {
446+
jestExpect(
447+
notCloseTo(expected, precision).asymmetricMatch(received),
448+
).toBe(true);
449+
});
450+
});
451+
452+
test('closeTo throw if expected is not number', () => {
453+
jestExpect(() => {
454+
closeTo('a');
455+
}).toThrow();
456+
});
457+
458+
test('notCloseTo throw if expected is not number', () => {
459+
jestExpect(() => {
460+
notCloseTo('a');
461+
}).toThrow();
462+
});
463+
464+
test('closeTo throw if precision is not number', () => {
465+
jestExpect(() => {
466+
closeTo(1, 'a');
467+
}).toThrow();
468+
});
469+
470+
test('notCloseTo throw if precision is not number', () => {
471+
jestExpect(() => {
472+
notCloseTo(1, 'a');
473+
}).toThrow();
474+
});
475+
476+
test('closeTo return false if received is not number', () => {
477+
jestExpect(closeTo(1).asymmetricMatch('a')).toBe(false);
478+
});
479+
480+
test('notCloseTo return false if received is not number', () => {
481+
jestExpect(notCloseTo(1).asymmetricMatch('a')).toBe(false);
482+
});
483+
});

packages/expect/src/asymmetricMatchers.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,46 @@ class StringMatching extends AsymmetricMatcher<RegExp> {
253253
return 'string';
254254
}
255255
}
256+
class CloseTo extends AsymmetricMatcher<number> {
257+
private precision: number;
258+
constructor(sample: number, precision: number = 2, inverse: boolean = false) {
259+
if (!isA('Number', sample)) {
260+
throw new Error('Expected is not a Number');
261+
}
262+
263+
if (!isA('Number', precision)) {
264+
throw new Error('Precision is not a Number');
265+
}
266+
267+
super(sample);
268+
this.inverse = inverse;
269+
this.precision = precision;
270+
}
271+
272+
asymmetricMatch(other: number) {
273+
if (!isA('Number', other)) {
274+
return false;
275+
}
276+
let result: boolean = false;
277+
if (other === Infinity && this.sample === Infinity) {
278+
result = true; // Infinity - Infinity is NaN
279+
} else if (other === -Infinity && this.sample === -Infinity) {
280+
result = true; // -Infinity - -Infinity is NaN
281+
} else {
282+
result =
283+
Math.abs(this.sample - other) < Math.pow(10, -this.precision) / 2;
284+
}
285+
return this.inverse ? !result : result;
286+
}
287+
288+
toString() {
289+
return `Number${this.inverse ? 'Not' : ''}CloseTo`;
290+
}
291+
292+
getExpectedType() {
293+
return 'number';
294+
}
295+
}
256296

257297
export const any = (expectedObject: unknown): Any => new Any(expectedObject);
258298
export const anything = (): Anything => new Anything();
@@ -274,3 +314,7 @@ export const stringMatching = (expected: string | RegExp): StringMatching =>
274314
new StringMatching(expected);
275315
export const stringNotMatching = (expected: string | RegExp): StringMatching =>
276316
new StringMatching(expected, true);
317+
export const closeTo = (expected: number, precision?: number): CloseTo =>
318+
new CloseTo(expected, precision);
319+
export const notCloseTo = (expected: number, precision?: number): CloseTo =>
320+
new CloseTo(expected, precision, true);

packages/expect/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
anything,
1515
arrayContaining,
1616
arrayNotContaining,
17+
closeTo,
18+
notCloseTo,
1719
objectContaining,
1820
objectNotContaining,
1921
stringContaining,
@@ -363,13 +365,15 @@ expect.any = any;
363365

364366
expect.not = {
365367
arrayContaining: arrayNotContaining,
368+
closeTo: notCloseTo,
366369
objectContaining: objectNotContaining,
367370
stringContaining: stringNotContaining,
368371
stringMatching: stringNotMatching,
369372
};
370373

371-
expect.objectContaining = objectContaining;
372374
expect.arrayContaining = arrayContaining;
375+
expect.closeTo = closeTo;
376+
expect.objectContaining = objectContaining;
373377
expect.stringContaining = stringContaining;
374378
expect.stringMatching = stringMatching;
375379

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy