Sinon: 未来里程碑的想法

创建于 2017-09-13  ·  31评论  ·  资料来源: sinonjs/sinon

背景

sinon.stub / sandbox.stub已成为厨房水槽
带有问题的可配置行为,这些问题通常很难在没有回归的情况下找到和修复。

我认为困难的根本原因是它stub的责任太多了。

此外, stub的使用也有问题,因为行为是在创建设置的,并且可以多次重新定义。

var myStub;

beforeEach(function(){
    myStub = sinon.stub().resolves('apple pie :)');
});

// several hundred lines of tests later
myStub = sinon.stub().rejects('no more pie :(');

// several hundred lines of tests later
// what behaviour does myStub currently have? Can you tell without 
// reading the entire file?
// can you safely change the behaviour without affecting tests further 
// down in the file?

然后是更令人困惑的场景

var myStub = sinon.stub()
                .withArgs(42)
                .onThirdCall()
                .resolves('apple pie')
                .rejects('no more pie')

那还能做什么?

与其继续为stub增加更多职责,我建议改为向sinon引入新成员,其范围要得多。

我能想到的最重要的是函数的不可变替代。

然后我们可以稍后弄清楚我们将对属性做什么(作为一个单独的、新的、单一职责的成员)。

sinon.fake

fake (调用sinon.fake的返回值)是纯粹且不可变Function 。 它只做一件事,而且只做一件事。 它在每次调用时都具有相同的行为。 与stub不同,它的行为不能被重新定义。 如果您需要不同的行为,请创建一个新的fake

单一责任

假货可以承担这些责任之一

  • Promise解析为一个值
  • 拒绝PromiseError
  • 返回一个值
  • 扔一个Error
  • 为回调产生值

如果您想要/需要副作用,并且仍然想要 spy 接口,那么要么使用真实函数,使用stub或制作自定义函数

sinon.replace(myObject, myMethod, sandbox.spy(function(args) {
    someFunctionWithSideEffects(args);
});

慷慨地抛出错误

当用户尝试以不受支持的方式创建/使用错误时,会慷慨地抛出错误。

// will throw TypeError when `config` argument has more than one property
const fake = sinon.fake({
    resolves: true, 
    returns: true
});

使用间谍 API

除了.withArgs ,因为这违反了不变性

使用思路

// will return a Promise that resolves to 'apple pie'
var fake = sinon.fake({resolves: 'apple pie'})

// will return a Promise that rejects with the provided Error, or 
// creates a generic Error using the input as message
var fake = sinon.fake({rejects: new TypeError('no more pie')});
var fake = sinon.fake({rejects: 'no more pie'});

// returns the value passed
var fake = sinon.fake({returns: 'apple pie'});

// throws the provided Error, or creates a generic Error using the 
// input as message
var fake = sinon.fake({throws: new RangeError('no more pie')});
var fake = sinon.fake({throws: 'no more pie'});

// replace a method with a fake
var fake = sinon.replace(myObject, 'methodName', sandbox.fake({
    returns: 'apple pie'
}))
// .. or use the helper method, which will use `sandbox.replace` and `
// sinon.fake`
var fake = sinon.setFake(global, 'methodName', {
    returns: 'apple pie'
});

// create an async fake
var asyncFake = sinon.asyncFake({
    returns: 'apple pie'
});

同义词

我不知道fake是否是这里使用的最佳名词,但我认为我们应该尽量坚持使用名词的惯例,不要误入形容词或动词。

提议的 API 更改

使用默认沙箱

这是我一直在考虑的事情,我们为什么不做一个默认的沙箱呢? 如果人们需要单独的沙箱,他们仍然可以创建它们。

我们应该创建一个默认沙箱,用于通过sinon.*公开的所有方法。
这意味着sinon.stub将与sandbox.stub相同,这将消除能够使用sinon.stub存根属性的限制。

sandbox.replace

创建sandbox.replace并将其用于在任何地方替换任何内容的所有操作。 将其公开为sinon.replace并在以这种方式使用时使用默认沙箱。

这可能应该有一些严肃的输入验证,所以它只会用函数替换函数,用访问器替换访问器等。

Feature Request Improvement Needs investigation pinned

最有用的评论

好吧,我想我们有类似的理解👍

只是为了重复一遍,以防我们遗漏了什么,以便其他贡献者有同样的理解。

TL;博士

  • 所有替换都将由一个新实用程序完成: sandbox.replace (目前位于stub中)
  • sinon将有一个默认沙箱,允许sinon.resetsinon.restore (我们应该合并它们吗?)
  • sinon.fake — 一个不可变的、可编程的替代记录所有调用的函数
  • sinon.spy
  • sinon.stub
// effectively a spy that has no target
const fake = sinon.fake()

// spy on a function
const fake = sinon.fake(console.log);
const fake = sinon.fake(function() { return 'apple pie'; });

// a shorthand construction of fake with behaviour
const fake = sinon.fake({
    returns: 'apple pie'
});

// replacing an existing function with a fake
var fakeLog = sinon.fake();
sandbox.replace(console, 'log', fakeLog);

在这种状态下,提案仅处理Function 。 我们需要考虑如何处理非函数属性和访问器。 至少,我们应该看看我们是否可以限制sandbox.replace只允许理智的替换。

所有31条评论

平 @sinonjs/核心

好建议,摩根。 感谢您提出这个问题。 我也认为stub API 很混乱,我喜欢你的所有建议。 以下是一些想法:

sinon.fake

我同意不变性是这里的关键。 不过,我们可以允许一些目前可以使用存根的“理智”用例。

例如,yield 和 return 可能是一个有效的用例:

sinon.fake({
  yields: [null, 42],
  returns: true
})

我们可以检查什么是有意义的,什么是没有意义的。

此外,如果我们支持callsThrough: true作为配置(与任何行为属性结合使用无效),也可以使用新的伪造品代替“间谍”API。 这将比学习诗浓语中的“间谍”和“存根”的含义更自我解释😄

使用默认沙箱

虽然我喜欢这个想法,但这意味着在测试之后调用sinon.restore()可能会恢复其他测试的一些剩余部分并导致令人惊讶的结果 - 或者之前发生的测试失败。 这样做的好处是重置beforeEach中的全局沙箱以改善测试隔离。 👍

沙盒.替换

我非常喜欢这个。 我将其理解为“让我把这个东西粘在那里”实用程序,对吧?

此外,如果我们支持 callThrough: true 作为配置(与任何行为属性组合无效),也可以使用新的伪造品代替“间谍”API。 这将比学习诗浓语中的“间谍”和“存根”的含义更自我解释😄

这是否意味着我们根本不需要spystub

沙盒.替换

我非常喜欢这个。 我将其理解为“让我把这个东西粘在那里”实用程序,对吧?

是的,就是这个想法。 与其重载相同的方法( sinon.stub )来做很多很多事情,不如使用只做一件事的显式方法

正如您所强调的, fake API 可能不会支持目前使用间谍和存根可能实现的所有功能。 但是,是的,我认为fake API 是一个统一stubspy功能的机会。

虽然我喜欢这个想法,但这意味着在测试之后调用 sinon.restore() 可以恢复其他测试的一些剩余部分并导致令人惊讶的结果 - 或者之前发生的测试失败。 这样做的好处是在 beforeEach 中重置全局沙箱以改进测试隔离。 👍

这无疑是一个突破性的变化,不应轻易引入。

创建fake时,如果您不向其传递行为配置,则它将等效于spy

// ~spy, records all calls, has no behaviour
const fake = sinon.fake();

// ~stub, records all calls, returns 'apple pie'
const fake = sinon.fake({
    returns: 'apple pie'
});

你将如何创建一个什么都不做的存根呢?

你将如何创建一个什么都不做的存根呢?

我不确定我是否完全理解你的问题......但是这里有

// a fake that has no behaviour
const fake = sinon.fake();

// put it in place of an existing method
sandbox.replace(myObject, 'someMethod', fake);

啊,我想我现在明白你的意思了: fake始终是stub 。 当您说~spy, records all calls时,我理解“调用原始函数”。 然而, fake并不知道它要替换的函数——这就是sandbox.replace所做的。

因此,考虑到这一点,这是另一个建议,我们如何将当前的spy功能(如在调用中)折叠到新的假货中:

const fake = sinon.fake(function () {
   // Any custom function
});

给定的函数将被伪造者调用。 此 API 使其无法与其他行为混合使用。 实际上,配置对象会创建一个实现指定行为的函数,然后将其传递给 fake。

然后sandbox.spy(object, method)实现可以变成这样:

const original = object[method];
const fake = sinon.fake(original);
sandbox.replace(object, method, fake);

基本上是单线🤓

是的。 一旦你简化了事情,那么你就可以开始重新混合以获得乐趣🎉和利润💰

但是,如果我们想倾向于只使用fake而不再使用spystub ,那么我们可能应该不理会这两个。

我正在考虑这里的“下一个”API。 您需要sandbox.spy才能在某处拥有替换逻辑。 据我了解,这应该是向后兼容的。 然后可以弃用stub实现。

您需要 sandbox.spy 在某处拥有替换逻辑。 据我了解,这应该是向后兼容的。 然后可以弃用存根实现。

我不确定我是否遵循。 你能详细说明一下吗?

当然。 据我了解您的建议,您希望替换过于复杂的stub API。 当前实现存根的方式是通过创建具有实现行为的函数的spy 。 我的建议是用fake API 做同样的事情并在内部创建一个spy ,但我们不会再返回一个行为,因为我们想摆脱链接. 我们只要把间谍还回去。 这使得fake实现成为stub的替代方案,返回的函数与所有当前的 Sinon API 兼容。 这有意义还是我错过了什么?

好吧,我想我们有类似的理解👍

只是为了重复一遍,以防我们遗漏了什么,以便其他贡献者有同样的理解。

TL;博士

  • 所有替换都将由一个新实用程序完成: sandbox.replace (目前位于stub中)
  • sinon将有一个默认沙箱,允许sinon.resetsinon.restore (我们应该合并它们吗?)
  • sinon.fake — 一个不可变的、可编程的替代记录所有调用的函数
  • sinon.spy
  • sinon.stub
// effectively a spy that has no target
const fake = sinon.fake()

// spy on a function
const fake = sinon.fake(console.log);
const fake = sinon.fake(function() { return 'apple pie'; });

// a shorthand construction of fake with behaviour
const fake = sinon.fake({
    returns: 'apple pie'
});

// replacing an existing function with a fake
var fakeLog = sinon.fake();
sandbox.replace(console, 'log', fakeLog);

在这种状态下,提案仅处理Function 。 我们需要考虑如何处理非函数属性和访问器。 至少,我们应该看看我们是否可以限制sandbox.replace只允许理智的替换。

这是否意味着sinon.stub()sinon.spy()都将在未来被弃用以支持sinon.fake() ,或者只是在内部重做? 如果是这样,那么我们本质上是在朝着TestDouble 的思路前进。 恕我直言,不一定是坏事,但可能值得考虑的是,如果很多人发现他们无论如何都需要为sinon.fake()替换所有 Sinon api 调用,他们不妨只使用另一个库(尽管这意味着他们将失去所有现有的 Sinon API 知识)。

这是否意味着 sinon.stub() 和 sinon.spy() 都将在未来被弃用以支持 sinon.fake(),或者只是在内部重做? 如果是这样,那么我们基本上是在朝着 TestDouble 的思路前进。

我想它确实有些重叠。 我提出这个建议的主要动机是拥有具有不可变行为的假函数。

恕我直言,不一定是坏事,但可能值得考虑的是,如果很多人发现他们无论如何都需要为 sinon.fake() 替换所有 Sinon api 调用,他们不妨只使用另一个库(尽管将意味着他们将失去所有现有的 Sinon API 知识)。

如果人们发现另一个图书馆可以更好地满足他们的需求,那么我很高兴我们帮助他们了解了这一点:)

但是我们会保留 spy 和 stub 方法还是弃用它们,并可能会减少一些功能? 我不清楚。

但是我们会保留 spy 和 stub 方法还是弃用它们,并可能会减少一些功能?

一旦fake看起来稳定,那么我会弃用spystub ,然后给它一年的时间让人们有时间升级。

我认为我们应该尽最大努力提供 codemods 和优秀的文档,以帮助人们移动他们的代码

我正在为此的第一部分(默认沙箱)开发一个分支。 我重构了代码,使sandboxcollection现在合二为一。 我已经让默认的沙箱工作了。

我将在接下来的几天内整理提交,然后在此存储库上为更新的 API 创建一个分支。

这是个好主意,顺便说一句,写得很好。

我还会向存根和间谍添加弃用通知。

我也在考虑通过传递函数来改变传递带有键的对象。

这将增加以下好处:

  • 这将允许我们为想要使用typescript或其他类型的静态检查器的用户添加type到那些功能
  • 用户在尝试为不存在的行为调用函数时会出错
  • 我们可以分别记录这些功能并使文档更好
  • 我们可以在传递对这些行为没有意义的参数时提供有用的错误,并允许它们具有可选/多个参数
  • 它也会使事情更加可组合(尽管在这种情况下我没有看到很多情况)并允许人们重用创建的行为
  • IMO 这也比拥有一个具有行为的对象更简单

因此,API 看起来像这样:

// It would be cool to allow users to import these using destructuring to make code more concise
import { resolves, rejects, returns } from 'sinon/behaviors'; 

var fake = sinon.fake(resolves('apple pie'))

var fake = sinon.fake(rejects(new TypeError('no more pie')));
var fake = sinon.fake(rejects('no more pie'));

var fake = sinon.fake(returns('apple pie'));

var fake = sinon.fake(throws(new RangeError('no more pie'));
var fake = sinon.fake(throws('no more pie'));

在实现这一点时,可能只是返回非常简单的对象,例如您提议的对象。 然后,如果我们有多个行为,我们可以合并它们。

此外,当涉及到混合onThirdCallwithArgs之类的东西时,我认为应该记录在这些情况下发生的事情。

很抱歉这么晚才审查这个。 过去几个月非常忙碌。

@lucasfcosta查看 PR #1586

此问题已自动标记为过时,因为它最近没有活动。 如果没有进一步的活动,它将被关闭。 感谢你的贡献。

5.0.0 以前的版本在 package.json 中与后来的 5.0.0-next.* 预发布版本造成问题,因为 5.0.0 比任何预发布版本都大。

由于 5.0.0 已经发布,我认为next预发布数字可能需要提高到5.0.1-next.1

我注意到了这一点,因为我正在使用的另一个包得到了一个弃用的 msg,它的 package.json 依赖于"sinon": "^5.0.0-next.4"

npm WARN deprecated [email protected]: this version has been deprecated

我不确定这是否值得为预发布问题打开一个新问题,所以这里的评论似乎最安全。

另一个解决方案是发布下一个主要版本。 你觉得@sinonjs/core 怎么样?

@mroderick我无法再告诉 v5 的所有更改是什么。 从我上次的测试来看,它运行良好,我期待使用新的假货。 这是一个新的专业,所以嘿,把它寄出去😄

在我们发布下一个主要版本之前,我只想合并一个 PR #1764。

我已经发布[email protected] ,希望这会让人们的生活更轻松。

谢谢,我已经测试了 package.json 中的依赖项(总是很好地仔细检查),并且"sinon": "^5.0.1"给出了一个错误,因为没有找到匹配项(还没有发布),并且"sinon": "^5.0.1-next.1"可以正常获取该版本。

这从来都不是什么大问题,我只是觉得值得让你知道,特别是当我看到 v5 已经开发了一段时间,所以我不知道它要发布多久。 我认为在不久的将来发布听起来是个好主意。

fake已与 #1768 一起引入,变为[email protected]

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

andys8 picture andys8  ·  4评论

OscarF picture OscarF  ·  4评论

NathanHazout picture NathanHazout  ·  3评论

stevenmusumeche picture stevenmusumeche  ·  3评论

stephanwlee picture stephanwlee  ·  3评论