【问题标题】:Testing floating point logic using chai-almost and sinon `calledWithMatch`使用 chai-almost 和 sinon `callWithMatch` 测试浮点逻辑
【发布时间】:2019-10-07 06:28:06
【问题描述】:

我有一个测试用例失败,因为正在测试的值被Number.EPSILON 关闭。我理解为什么会发生这种情况,并相信我需要更改我的测试用例,以便它能够容忍这种差异。我相信使用chai-almost 来协助这件事是有道理的,但我正在努力弄清楚如何将chai-almostsinon-chai 集成并正在寻找想法。

具体来说,我使用的是sinon-chai提供的calledWithMatch方法。 calledWithMatch 方法在两个对象之间执行深度相等性检查,并且不考虑引用相等性。我想放宽这种方法以容忍Number.EPSILON 的差异。

下面的代码 sn-p 突出显示了测试用例失败的问题。测试用例失败是因为persist 被调用时使用了一个边界框,由于topNumber.EPSILON 关闭,该边界框未能达到我们的预期。在这种情况下,测试用例应该可以通过,因为数据没有错误。

mocha.setup('bdd');

const updater = {
  updateBoundingBox(boundingBox) {
    const newBoundingBox = { ...boundingBox };
    newBoundingBox.top -= .2;
    newBoundingBox.top += .2;  
    this.persist(newBoundingBox);
  },
  
  persist(boundingBox) {
    console.log('persisting bounding box', boundingBox);
  }
};

describe('example', () => {
  it('should pass', () => {
    const persistSpy = sinon.spy(updater, 'persist');

    const originalBoundingBox = {
      top: 0.01,
      left: 0.01,
      bottom: 0.01,
      right: 0.01,
    };
    updater.updateBoundingBox(originalBoundingBox);
    chai.expect(persistSpy).calledWithMatch(originalBoundingBox);
  });
});

mocha.run();
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/6.1.4/mocha.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/7.3.2/sinon.min.js"></script>
<script>
"use strict";
/* eslint-disable no-invalid-this */

(function (sinonChai) {
    // Module systems magic dance.

    /* istanbul ignore else */
    if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
        // NodeJS
        module.exports = sinonChai;
    } else if (typeof define === "function" && define.amd) {
        // AMD
        define(function () {
            return sinonChai;
        });
    } else {
        // Other environment (usually <script> tag): plug in to global chai instance directly.
        /* global chai: false */
        chai.use(sinonChai);
    }
}(function (chai, utils) {
    var slice = Array.prototype.slice;

    function isSpy(putativeSpy) {
        return typeof putativeSpy === "function" &&
               typeof putativeSpy.getCall === "function" &&
               typeof putativeSpy.calledWithExactly === "function";
    }

    function timesInWords(count) {
        switch (count) {
            case 1: {
                return "once";
            }
            case 2: {
                return "twice";
            }
            case 3: {
                return "thrice";
            }
            default: {
                return (count || 0) + " times";
            }
        }
    }

    function isCall(putativeCall) {
        return putativeCall && isSpy(putativeCall.proxy);
    }

    function assertCanWorkWith(assertion) {
        if (!isSpy(assertion._obj) && !isCall(assertion._obj)) {
            throw new TypeError(utils.inspect(assertion._obj) + " is not a spy or a call to a spy!");
        }
    }

    function getMessages(spy, action, nonNegatedSuffix, always, args) {
        var verbPhrase = always ? "always have " : "have ";
        nonNegatedSuffix = nonNegatedSuffix || "";
        if (isSpy(spy.proxy)) {
            spy = spy.proxy;
        }

        function printfArray(array) {
            return spy.printf.apply(spy, array);
        }

        return {
            affirmative: function () {
                return printfArray(["expected %n to " + verbPhrase + action + nonNegatedSuffix].concat(args));
            },
            negative: function () {
                return printfArray(["expected %n to not " + verbPhrase + action].concat(args));
            }
        };
    }

    function sinonProperty(name, action, nonNegatedSuffix) {
        utils.addProperty(chai.Assertion.prototype, name, function () {
            assertCanWorkWith(this);

            var messages = getMessages(this._obj, action, nonNegatedSuffix, false);
            this.assert(this._obj[name], messages.affirmative, messages.negative);
        });
    }

    function sinonPropertyAsBooleanMethod(name, action, nonNegatedSuffix) {
        utils.addMethod(chai.Assertion.prototype, name, function (arg) {
            assertCanWorkWith(this);

            var messages = getMessages(this._obj, action, nonNegatedSuffix, false, [timesInWords(arg)]);
            this.assert(this._obj[name] === arg, messages.affirmative, messages.negative);
        });
    }

    function createSinonMethodHandler(sinonName, action, nonNegatedSuffix) {
        return function () {
            assertCanWorkWith(this);

            var alwaysSinonMethod = "always" + sinonName[0].toUpperCase() + sinonName.substring(1);
            var shouldBeAlways = utils.flag(this, "always") && typeof this._obj[alwaysSinonMethod] === "function";
            var sinonMethodName = shouldBeAlways ? alwaysSinonMethod : sinonName;

            var messages = getMessages(this._obj, action, nonNegatedSuffix, shouldBeAlways, slice.call(arguments));
            this.assert(
                this._obj[sinonMethodName].apply(this._obj, arguments),
                messages.affirmative,
                messages.negative
            );
        };
    }

    function sinonMethodAsProperty(name, action, nonNegatedSuffix) {
        var handler = createSinonMethodHandler(name, action, nonNegatedSuffix);
        utils.addProperty(chai.Assertion.prototype, name, handler);
    }

    function exceptionalSinonMethod(chaiName, sinonName, action, nonNegatedSuffix) {
        var handler = createSinonMethodHandler(sinonName, action, nonNegatedSuffix);
        utils.addMethod(chai.Assertion.prototype, chaiName, handler);
    }

    function sinonMethod(name, action, nonNegatedSuffix) {
        exceptionalSinonMethod(name, name, action, nonNegatedSuffix);
    }

    utils.addProperty(chai.Assertion.prototype, "always", function () {
        utils.flag(this, "always", true);
    });

    sinonProperty("called", "been called", " at least once, but it was never called");
    sinonPropertyAsBooleanMethod("callCount", "been called exactly %1", ", but it was called %c%C");
    sinonProperty("calledOnce", "been called exactly once", ", but it was called %c%C");
    sinonProperty("calledTwice", "been called exactly twice", ", but it was called %c%C");
    sinonProperty("calledThrice", "been called exactly thrice", ", but it was called %c%C");
    sinonMethodAsProperty("calledWithNew", "been called with new");
    sinonMethod("calledBefore", "been called before %1");
    sinonMethod("calledAfter", "been called after %1");
    sinonMethod("calledImmediatelyBefore", "been called immediately before %1");
    sinonMethod("calledImmediatelyAfter", "been called immediately after %1");
    sinonMethod("calledOn", "been called with %1 as this", ", but it was called with %t instead");
    sinonMethod("calledWith", "been called with arguments %*", "%D");
    sinonMethod("calledOnceWith", "been called exactly once with arguments %*", "%D");
    sinonMethod("calledWithExactly", "been called with exact arguments %*", "%D");
    sinonMethod("calledOnceWithExactly", "been called exactly once with exact arguments %*", "%D");
    sinonMethod("calledWithMatch", "been called with arguments matching %*", "%D");
    sinonMethod("returned", "returned %1");
    exceptionalSinonMethod("thrown", "threw", "thrown %1");
}));
</script>

<div id="mocha"></div>

我不确定从这里去哪里。如果我直接使用这两个实体,而不是使用calledWithMatch,我会使用chai-almost 明确检查topbottomleftright 值。类似于:

expect(newBoundingBox.top).to.almost.equal(boundingBox.top)
expect(newBoundingBox.bottom).to.almost.equal(boundingBox.bottom)
expect(newBoundingBox.left).to.almost.equal(boundingBox.left)
expect(newBoundingBox.right).to.almost.equal(boundingBox.right)

但在使用calledWithMatch 时,我还没有看到实现此目的的方法。

我错过了什么吗?有没有简单的方法?

编辑:只是在我修补时更新它。

我认为正确的方法可能是使用自定义匹配器,但我还没有工作代码:https://sinonjs.org/releases/latest/matchers/#custom-matchers

看起来calledWithMatch(foo) 的功能等价物是calledWith(sinon.match(foo)),这使得如何引入自定义匹配器的使用更加清晰。

【问题讨论】:

    标签: javascript floating-point chai sinon


    【解决方案1】:

    好吧,我想通了。

    诀窍是将sinon-chai 方法calledWithMatch 替换为其较低级别的实现calledWith(sinon.match。这允许定义一个自定义匹配器。我在下面的示例中展示了我使用的那个。

    mocha.setup('bdd');
    
    const updater = {
      updateBoundingBox(boundingBox) {
        const newBoundingBox = { ...boundingBox };
        newBoundingBox.top -= .2;
        newBoundingBox.top += .2;  
        this.persist(newBoundingBox);
      },
      
      persist(boundingBox) {
        console.log('persisting bounding box', boundingBox);
      }
    };
    
    describe('example', () => {
      it('should pass', () => {
        const persistSpy = sinon.spy(updater, 'persist');
    
        const originalBoundingBox = {
          top: 0.01,
          left: 0.01,
          bottom: 0.01,
          right: 0.01,
        };
        updater.updateBoundingBox(originalBoundingBox);
        chai.expect(persistSpy).calledWith(sinon.match((boundingBox) => {
            if (!boundingBox) return false;
            
            const isLeftEqual = Math.abs(originalBoundingBox.left - boundingBox.left) < Number.EPSILON;
            const isRightEqual = Math.abs(originalBoundingBox.right - boundingBox.right) < Number.EPSILON;
            const isTopEqual = Math.abs(originalBoundingBox.top - boundingBox.top) < Number.EPSILON;
            const isBottomEqual = Math.abs(originalBoundingBox.bottom - boundingBox.bottom) < Number.EPSILON;
            
            return isLeftEqual && isRightEqual && isTopEqual && isBottomEqual; 
        }));
      });
    });
    
    mocha.run();
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/6.1.4/mocha.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.2.0/chai.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/sinon.js/7.3.2/sinon.min.js"></script>
    <script>
    "use strict";
    /* eslint-disable no-invalid-this */
    
    (function (sinonChai) {
        // Module systems magic dance.
    
        /* istanbul ignore else */
        if (typeof require === "function" && typeof exports === "object" && typeof module === "object") {
            // NodeJS
            module.exports = sinonChai;
        } else if (typeof define === "function" && define.amd) {
            // AMD
            define(function () {
                return sinonChai;
            });
        } else {
            // Other environment (usually <script> tag): plug in to global chai instance directly.
            /* global chai: false */
            chai.use(sinonChai);
        }
    }(function (chai, utils) {
        var slice = Array.prototype.slice;
    
        function isSpy(putativeSpy) {
            return typeof putativeSpy === "function" &&
                   typeof putativeSpy.getCall === "function" &&
                   typeof putativeSpy.calledWithExactly === "function";
        }
    
        function timesInWords(count) {
            switch (count) {
                case 1: {
                    return "once";
                }
                case 2: {
                    return "twice";
                }
                case 3: {
                    return "thrice";
                }
                default: {
                    return (count || 0) + " times";
                }
            }
        }
    
        function isCall(putativeCall) {
            return putativeCall && isSpy(putativeCall.proxy);
        }
    
        function assertCanWorkWith(assertion) {
            if (!isSpy(assertion._obj) && !isCall(assertion._obj)) {
                throw new TypeError(utils.inspect(assertion._obj) + " is not a spy or a call to a spy!");
            }
        }
    
        function getMessages(spy, action, nonNegatedSuffix, always, args) {
            var verbPhrase = always ? "always have " : "have ";
            nonNegatedSuffix = nonNegatedSuffix || "";
            if (isSpy(spy.proxy)) {
                spy = spy.proxy;
            }
    
            function printfArray(array) {
                return spy.printf.apply(spy, array);
            }
    
            return {
                affirmative: function () {
                    return printfArray(["expected %n to " + verbPhrase + action + nonNegatedSuffix].concat(args));
                },
                negative: function () {
                    return printfArray(["expected %n to not " + verbPhrase + action].concat(args));
                }
            };
        }
    
        function sinonProperty(name, action, nonNegatedSuffix) {
            utils.addProperty(chai.Assertion.prototype, name, function () {
                assertCanWorkWith(this);
    
                var messages = getMessages(this._obj, action, nonNegatedSuffix, false);
                this.assert(this._obj[name], messages.affirmative, messages.negative);
            });
        }
    
        function sinonPropertyAsBooleanMethod(name, action, nonNegatedSuffix) {
            utils.addMethod(chai.Assertion.prototype, name, function (arg) {
                assertCanWorkWith(this);
    
                var messages = getMessages(this._obj, action, nonNegatedSuffix, false, [timesInWords(arg)]);
                this.assert(this._obj[name] === arg, messages.affirmative, messages.negative);
            });
        }
    
        function createSinonMethodHandler(sinonName, action, nonNegatedSuffix) {
            return function () {
                assertCanWorkWith(this);
    
                var alwaysSinonMethod = "always" + sinonName[0].toUpperCase() + sinonName.substring(1);
                var shouldBeAlways = utils.flag(this, "always") && typeof this._obj[alwaysSinonMethod] === "function";
                var sinonMethodName = shouldBeAlways ? alwaysSinonMethod : sinonName;
    
                var messages = getMessages(this._obj, action, nonNegatedSuffix, shouldBeAlways, slice.call(arguments));
                this.assert(
                    this._obj[sinonMethodName].apply(this._obj, arguments),
                    messages.affirmative,
                    messages.negative
                );
            };
        }
    
        function sinonMethodAsProperty(name, action, nonNegatedSuffix) {
            var handler = createSinonMethodHandler(name, action, nonNegatedSuffix);
            utils.addProperty(chai.Assertion.prototype, name, handler);
        }
    
        function exceptionalSinonMethod(chaiName, sinonName, action, nonNegatedSuffix) {
            var handler = createSinonMethodHandler(sinonName, action, nonNegatedSuffix);
            utils.addMethod(chai.Assertion.prototype, chaiName, handler);
        }
    
        function sinonMethod(name, action, nonNegatedSuffix) {
            exceptionalSinonMethod(name, name, action, nonNegatedSuffix);
        }
    
        utils.addProperty(chai.Assertion.prototype, "always", function () {
            utils.flag(this, "always", true);
        });
    
        sinonProperty("called", "been called", " at least once, but it was never called");
        sinonPropertyAsBooleanMethod("callCount", "been called exactly %1", ", but it was called %c%C");
        sinonProperty("calledOnce", "been called exactly once", ", but it was called %c%C");
        sinonProperty("calledTwice", "been called exactly twice", ", but it was called %c%C");
        sinonProperty("calledThrice", "been called exactly thrice", ", but it was called %c%C");
        sinonMethodAsProperty("calledWithNew", "been called with new");
        sinonMethod("calledBefore", "been called before %1");
        sinonMethod("calledAfter", "been called after %1");
        sinonMethod("calledImmediatelyBefore", "been called immediately before %1");
        sinonMethod("calledImmediatelyAfter", "been called immediately after %1");
        sinonMethod("calledOn", "been called with %1 as this", ", but it was called with %t instead");
        sinonMethod("calledWith", "been called with arguments %*", "%D");
        sinonMethod("calledOnceWith", "been called exactly once with arguments %*", "%D");
        sinonMethod("calledWithExactly", "been called with exact arguments %*", "%D");
        sinonMethod("calledOnceWithExactly", "been called exactly once with exact arguments %*", "%D");
        sinonMethod("calledWithMatch", "been called with arguments matching %*", "%D");
        sinonMethod("returned", "returned %1");
        exceptionalSinonMethod("thrown", "threw", "thrown %1");
    }));
    </script>
    
    <div id="mocha"></div>

    【讨论】:

      猜你喜欢
      • 2015-12-01
      • 1970-01-01
      • 2021-12-10
      • 1970-01-01
      • 2020-10-11
      • 2018-04-08
      • 2016-01-25
      • 2015-10-01
      • 1970-01-01
      相关资源
      最近更新 更多