diff --git a/docs/api/assert/closeTo.md b/docs/api/assert/closeTo.md new file mode 100644 index 000000000..b0612b890 --- /dev/null +++ b/docs/api/assert/closeTo.md @@ -0,0 +1,64 @@ +--- +layout: page-api +title: assert.closeTo() +excerpt: Compare that a number is equal within a given tolerance. +groups: + - assert +redirect_from: + - "/assert/closeTo/" +version_added: "unreleased" +--- + +`closeTo( actual, expected, delta, message = "" )` + +Compare that a number is equal to a known target number within a given tolerance. + +| name | description | +|------|-------------| +| `actual` (number) | Expression being tested | +| `expected` (number) | Known target number | +| `delta` (number) | The maximum difference between the expected and actual number | +| `message` (string) | Optional description of the actual expression | + +The `assert.closeTo()` assertion checks that the actual expression approximates the expected number, allowing it to be off by at most the specified amount ("delta"). This can be used to assert that two numbers are roughly or almost equal to each other. + +The actual number may be either above or below the expected number, as long as it is within the `delta` difference (inclusive). + +While non-strict assertions like this are [often discouraged](https://timotijhof.net/posts/2015/qunit-anti-patterns/), it may be necessary to account for limitations in how fractional numbers are represented in JavaScript. For example, `0.1 + 0.2` is actually `0.30000000000000004`. This because math operations in JavaScript adhere to the "IEEE floating-point" standard. + +To learn how floating-point numbers work internally, refer to [Double-precision floating-point format](https://en.wikipedia.org/wiki/Double-precision_floating-point_format) on Wikipedia. To learn why floating-point numbers experience these side effects, refer to "[What Every Computer Scientist Should Know About Floating-Point Arithmetic](http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html)" by David Goldberg. + +## Examples + +```js +QUnit.test('good example', assert => { + const x = 0.1 + 0.2; // 0.30000000000000004 + + // passing: must be between 0.299 and 0.301 + assert.closeTo(x, 0.3, 0.001); + + const y = 20.13; + // passing: must be between 20.05 and 20.15 inclusive + assert.closeTo(y, 20.10, 0.05); +}); + +QUnit.test('bad example', assert => { + const x = 20.7; + // failing: must be between 20.0 and 20.2 inclusive + assert.closeTo(x, 20.1, 0.1); + // message: value should be within 0.1 inclusive + // actual : 20.7 + // expected: 20.1 + + const y = 2018; + // failing: must be between 2010 and 2014 inclusive + assert.closeTo(y, 2012, 2); + // message: value should be within 2 inclusive + // actual : 2018 + // expected: 2012 +}); +``` + +## See also + +* Use [`assert.propContains()`](./propContains.md) to partially compare an object. diff --git a/src/assert.js b/src/assert.js index 94cc8e7f7..f00ba8d4c 100644 --- a/src/assert.js +++ b/src/assert.js @@ -77,6 +77,18 @@ class Assert { return this.test.internalStop(requiredCalls); } + closeTo (actual, expected, delta, message) { + if (typeof delta !== 'number') { + throw new TypeError('closeTo() requires a delta argument'); + } + this.pushResult({ + result: Math.abs(actual - expected) <= delta, + actual, + expected, + message: message || `value should be within ${delta} inclusive` + }); + } + // Exports test.push() to the user API // Alias of pushResult. push (result, actual, expected, message, negative) { diff --git a/test/cli/fixtures/assert-failure.js b/test/cli/fixtures/assert-failure.js new file mode 100644 index 000000000..15a3c4406 --- /dev/null +++ b/test/cli/fixtures/assert-failure.js @@ -0,0 +1,27 @@ +// For passing tests, see /test/main/assert.js +// +// TODO: After we migrate running of tests in browsers to use TAP, +// merge these two files and verify them by TAP output instead of +// by boolean passing (akin to what we do with Node.js already). + +QUnit.module('assert', function () { + QUnit.test('true [failure]', function (assert) { + assert.true(false); + }); + + QUnit.test('false [failure]', function (assert) { + assert.false(true); + }); + + QUnit.test('closeTo [failure]', function (assert) { + assert.closeTo(1, 2, 0); + assert.closeTo(1, 2, 1); + assert.closeTo(2, 7, 1); + + assert.closeTo(7, 7.3, 0.1); + assert.closeTo(7, 7.3, 0.2); + assert.closeTo(2011, 2013, 1); + + assert.closeTo(20.7, 20.1, 0.1); + }); +}); diff --git a/test/cli/fixtures/assert-failure.tap.txt b/test/cli/fixtures/assert-failure.tap.txt new file mode 100644 index 000000000..3b5dabde8 --- /dev/null +++ b/test/cli/fixtures/assert-failure.tap.txt @@ -0,0 +1,78 @@ +# name: test with failing assertion +# command: ["qunit","assert-failure.js"] + +TAP version 13 +not ok 1 assert > true [failure] + --- + message: failed + severity: failed + actual : false + expected: true + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:9:16 + ... +not ok 2 assert > false [failure] + --- + message: failed + severity: failed + actual : true + expected: false + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:13:17 + ... +not ok 3 assert > closeTo [failure] + --- + message: value should be within 0 inclusive + severity: failed + actual : 1 + expected: 2 + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:17:12 + ... + --- + message: value should be within 1 inclusive + severity: failed + actual : 2 + expected: 7 + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:19:12 + ... + --- + message: value should be within 0.1 inclusive + severity: failed + actual : 7 + expected: 7.3 + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:21:12 + ... + --- + message: value should be within 0.2 inclusive + severity: failed + actual : 7 + expected: 7.3 + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:22:12 + ... + --- + message: value should be within 1 inclusive + severity: failed + actual : 2011 + expected: 2013 + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:23:12 + ... + --- + message: value should be within 0.1 inclusive + severity: failed + actual : 20.7 + expected: 20.1 + stack: | + at /qunit/test/cli/fixtures/assert-failure.js:25:12 + ... +1..3 +# pass 0 +# skip 0 +# todo 0 +# fail 3 + +# exit code: 1 diff --git a/test/cli/fixtures/failure.js b/test/cli/fixtures/failure.js deleted file mode 100644 index ecb7959ef..000000000 --- a/test/cli/fixtures/failure.js +++ /dev/null @@ -1,9 +0,0 @@ -QUnit.module('Failure', function () { - QUnit.test('bad', function (assert) { - assert.true(false); - }); - - QUnit.test('bad again', function (assert) { - assert.true(false); - }); -}); diff --git a/test/cli/fixtures/failure.tap.txt b/test/cli/fixtures/failure.tap.txt deleted file mode 100644 index e2c5b0d7f..000000000 --- a/test/cli/fixtures/failure.tap.txt +++ /dev/null @@ -1,29 +0,0 @@ -# name: test with failing assertion -# command: ["qunit","failure.js"] - -TAP version 13 -not ok 1 Failure > bad - --- - message: failed - severity: failed - actual : false - expected: true - stack: | - at /qunit/test/cli/fixtures/failure.js:3:16 - ... -not ok 2 Failure > bad again - --- - message: failed - severity: failed - actual : false - expected: true - stack: | - at /qunit/test/cli/fixtures/failure.js:7:16 - ... -1..2 -# pass 0 -# skip 0 -# todo 0 -# fail 2 - -# exit code: 1 \ No newline at end of file diff --git a/test/main/assert.js b/test/main/assert.js index 2255aabc3..528f1e482 100644 --- a/test/main/assert.js +++ b/test/main/assert.js @@ -92,6 +92,27 @@ QUnit.test('notStrictEqual', function (assert) { assert.notStrictEqual('foo', { toString: function () { return 'foo'; } }); }); +QUnit.test('closeTo', function (assert) { + assert.closeTo(1, 1, 0); + assert.closeTo(1, 1, 0.1); + + assert.closeTo(7, 7.1, 0.1); + assert.closeTo(7, 7.1, 0.2); + + assert.closeTo(2011, 2013, 2); + + assert.closeTo(0.1 + 0.2, 0.3, 0.001); + assert.closeTo(20.13, 20.10, 0.05); + + assert.throws(function () { + assert.closeTo(1, 1); + }, TypeError, 'missing delta'); + + assert.throws(function () { + assert.closeTo(1, 1, false); + }, TypeError, 'invalid delta'); +}); + QUnit.test('propEqual', function (assert) { function Foo (x, y, z) { this.x = x;