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

Diff for: CHANGELOG.md

+1
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

Diff for: docs/ExpectAPI.md

+20
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.

Diff for: packages/expect/src/__tests__/asymmetricMatchers.test.ts

+104
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+
});

Diff for: packages/expect/src/asymmetricMatchers.ts

+44
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);

Diff for: packages/expect/src/index.ts

+5-1
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)