Skip to content

Commit

Permalink
feat(expect): add asymmetric matcher expect.closeTo (#12243)
Browse files Browse the repository at this point in the history
  • Loading branch information
Biki-das committed Feb 5, 2022
1 parent 33b2cc0 commit 5160ae0
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

### Features

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

### Fixes
Expand Down
20 changes: 20 additions & 0 deletions docs/ExpectAPI.md
Expand Up @@ -432,6 +432,26 @@ test('doAsync calls both callbacks', () => {

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

### `expect.closeTo(number, numDigits?)`

`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.

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)`.

For example, this test passes with a precision of 5 digits:

```js
test('compare float in object properties', () => {
expect({
title: '0.1 + 0.2',
sum: 0.1 + 0.2,
}).toEqual({
title: '0.1 + 0.2',
sum: expect.closeTo(0.3, 5),
});
});
```

### `expect.hasAssertions()`

`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.
Expand Down
104 changes: 104 additions & 0 deletions packages/expect/src/__tests__/asymmetricMatchers.test.ts
Expand Up @@ -12,6 +12,8 @@ import {
anything,
arrayContaining,
arrayNotContaining,
closeTo,
notCloseTo,
objectContaining,
objectNotContaining,
stringContaining,
Expand Down Expand Up @@ -377,3 +379,105 @@ test('StringNotMatching throws if expected value is neither string nor regexp',
test('StringNotMatching returns true if received value is not string', () => {
jestExpect(stringNotMatching('en').asymmetricMatch(1)).toBe(true);
});

describe('closeTo', () => {
[
[0, 0],
[0, 0.001],
[1.23, 1.229],
[1.23, 1.226],
[1.23, 1.225],
[1.23, 1.234],
[Infinity, Infinity],
[-Infinity, -Infinity],
].forEach(([expected, received]) => {
test(`${expected} closeTo ${received} return true`, () => {
jestExpect(closeTo(expected).asymmetricMatch(received)).toBe(true);
});
test(`${expected} notCloseTo ${received} return false`, () => {
jestExpect(notCloseTo(expected).asymmetricMatch(received)).toBe(false);
});
});

[
[0, 0.01],
[1, 1.23],
[1.23, 1.2249999],
[Infinity, -Infinity],
[Infinity, 1.23],
[-Infinity, -1.23],
].forEach(([expected, received]) => {
test(`${expected} closeTo ${received} return false`, () => {
jestExpect(closeTo(expected).asymmetricMatch(received)).toBe(false);
});
test(`${expected} notCloseTo ${received} return true`, () => {
jestExpect(notCloseTo(expected).asymmetricMatch(received)).toBe(true);
});
});

[
[0, 0.1, 0],
[0, 0.0001, 3],
[0, 0.000004, 5],
[2.0000002, 2, 5],
].forEach(([expected, received, precision]) => {
test(`${expected} closeTo ${received} with precision ${precision} return true`, () => {
jestExpect(closeTo(expected, precision).asymmetricMatch(received)).toBe(
true,
);
});
test(`${expected} notCloseTo ${received} with precision ${precision} return false`, () => {
jestExpect(
notCloseTo(expected, precision).asymmetricMatch(received),
).toBe(false);
});
});

[
[3.141592e-7, 3e-7, 8],
[56789, 51234, -4],
].forEach(([expected, received, precision]) => {
test(`${expected} closeTo ${received} with precision ${precision} return false`, () => {
jestExpect(closeTo(expected, precision).asymmetricMatch(received)).toBe(
false,
);
});
test(`${expected} notCloseTo ${received} with precision ${precision} return true`, () => {
jestExpect(
notCloseTo(expected, precision).asymmetricMatch(received),
).toBe(true);
});
});

test('closeTo throw if expected is not number', () => {
jestExpect(() => {
closeTo('a');
}).toThrow();
});

test('notCloseTo throw if expected is not number', () => {
jestExpect(() => {
notCloseTo('a');
}).toThrow();
});

test('closeTo throw if precision is not number', () => {
jestExpect(() => {
closeTo(1, 'a');
}).toThrow();
});

test('notCloseTo throw if precision is not number', () => {
jestExpect(() => {
notCloseTo(1, 'a');
}).toThrow();
});

test('closeTo return false if received is not number', () => {
jestExpect(closeTo(1).asymmetricMatch('a')).toBe(false);
});

test('notCloseTo return false if received is not number', () => {
jestExpect(notCloseTo(1).asymmetricMatch('a')).toBe(false);
});
});
44 changes: 44 additions & 0 deletions packages/expect/src/asymmetricMatchers.ts
Expand Up @@ -253,6 +253,46 @@ class StringMatching extends AsymmetricMatcher<RegExp> {
return 'string';
}
}
class CloseTo extends AsymmetricMatcher<number> {
private precision: number;
constructor(sample: number, precision: number = 2, inverse: boolean = false) {
if (!isA('Number', sample)) {
throw new Error('Expected is not a Number');
}

if (!isA('Number', precision)) {
throw new Error('Precision is not a Number');
}

super(sample);
this.inverse = inverse;
this.precision = precision;
}

asymmetricMatch(other: number) {
if (!isA('Number', other)) {
return false;
}
let result: boolean = false;
if (other === Infinity && this.sample === Infinity) {
result = true; // Infinity - Infinity is NaN
} else if (other === -Infinity && this.sample === -Infinity) {
result = true; // -Infinity - -Infinity is NaN
} else {
result =
Math.abs(this.sample - other) < Math.pow(10, -this.precision) / 2;
}
return this.inverse ? !result : result;
}

toString() {
return `Number${this.inverse ? 'Not' : ''}CloseTo`;
}

getExpectedType() {
return 'number';
}
}

export const any = (expectedObject: unknown): Any => new Any(expectedObject);
export const anything = (): Anything => new Anything();
Expand All @@ -274,3 +314,7 @@ export const stringMatching = (expected: string | RegExp): StringMatching =>
new StringMatching(expected);
export const stringNotMatching = (expected: string | RegExp): StringMatching =>
new StringMatching(expected, true);
export const closeTo = (expected: number, precision?: number): CloseTo =>
new CloseTo(expected, precision);
export const notCloseTo = (expected: number, precision?: number): CloseTo =>
new CloseTo(expected, precision, true);
6 changes: 5 additions & 1 deletion packages/expect/src/index.ts
Expand Up @@ -14,6 +14,8 @@ import {
anything,
arrayContaining,
arrayNotContaining,
closeTo,
notCloseTo,
objectContaining,
objectNotContaining,
stringContaining,
Expand Down Expand Up @@ -363,13 +365,15 @@ expect.any = any;

expect.not = {
arrayContaining: arrayNotContaining,
closeTo: notCloseTo,
objectContaining: objectNotContaining,
stringContaining: stringNotContaining,
stringMatching: stringNotMatching,
};

expect.objectContaining = objectContaining;
expect.arrayContaining = arrayContaining;
expect.closeTo = closeTo;
expect.objectContaining = objectContaining;
expect.stringContaining = stringContaining;
expect.stringMatching = stringMatching;

Expand Down

0 comments on commit 5160ae0

Please sign in to comment.