首页 >web前端 >js教程 >Sinon教程:使用模拟,间谍和存根的JavaScript测试

Sinon教程:使用模拟,间谍和存根的JavaScript测试

Joseph Gordon-Levitt
Joseph Gordon-Levitt原创
2025-02-18 10:13:13700浏览

Sinon Tutorial: JavaScript Testing with Mocks, Spies & Stubs

本文由 Mark Brown 和 Marc Towler 审核。感谢所有 SitePoint 的同行评审员,使 SitePoint 的内容达到最佳状态!

编写单元测试时,最大的障碍之一是如何处理非平凡的代码。

在实际项目中,代码经常执行各种使测试变得困难的操作。Ajax 请求、计时器、日期、访问其他浏览器功能……或者如果您使用的是 Node.js,数据库总是很有趣,网络或文件访问也是如此。

所有这些都很难测试,因为您无法在代码中控制它们。如果您使用的是 Ajax,则需要一个服务器来响应请求,以便使您的测试通过。如果您使用 setTimeout,您的测试将不得不等待。对于数据库或网络,情况也是一样——您需要一个包含正确数据的数据库,或一个网络服务器。

现实生活不像许多测试教程看起来那样容易。但您知道有解决方案吗?

通过使用 Sinon,我们可以使测试非平凡的代码变得微不足道!

让我们看看它是如何工作的。

关键要点

  • Sinon 简化测试:Sinon.js 对于简化涉及复杂操作(如 Ajax 调用、计时器和数据库交互)的 JavaScript 代码的测试至关重要,因为它允许用模拟、间谍和存根替换这些部分。
  • 三种类型的测试替身:Sinon 将测试替身分为间谍(收集有关函数调用的信息);存根(可以替换函数以强制执行特定行为);以及模拟(非常适合替换和断言整个对象的行为)。
  • 实用案例:Sinon 在单元测试场景中特别有用,在这些场景中,外部依赖项会使测试复杂化或减慢测试速度,例如外部 API 调用或基于时间的函数。
  • 集成和设置:Sinon 可以轻松集成到 Node.js 和基于浏览器的测试环境中,增强其在各种 JavaScript 应用程序中的多功能性和易用性。
  • 增强的断言:Sinon 提供增强的断言方法,这些方法会生成更清晰的错误消息,从而改进测试失败期间的调试过程。
  • 最佳实践:使用 sinon.test() 包装测试用例可确保正确清理测试替身,防止在其他测试中产生副作用,并减少测试套件中潜在的错误。

是什么使 Sinon 如此重要和有用?

简而言之,Sinon 允许您将测试的困难部分替换为使测试变得简单的部分。

在测试一段代码时,您不希望它受到测试外部任何因素的影响。如果某些外部因素影响测试,则测试会变得更加复杂,并且可能会随机失败。

如果您想测试进行 Ajax 调用的代码,该如何操作?您需要运行一个服务器并确保它提供测试所需的精确响应。设置起来很复杂,并且使编写和运行单元测试变得困难。

如果您的代码依赖于时间怎么办?假设它等待一秒钟后再执行某些操作。现在怎么办?您可以在测试中使用 setTimeout 来等待一秒钟,但这会使测试变慢。想象一下,如果间隔更长,例如五分钟。我猜您可能不想每次运行测试时都等待五分钟。

通过使用 Sinon,我们可以解决这两个问题(以及许多其他问题),并消除复杂性。

Sinon 如何工作?

Sinon 通过允许您轻松创建所谓的 测试替身 来帮助消除测试中的复杂性。

顾名思义,测试替身是测试中使用的代码片段的替代品。回顾 Ajax 示例,我们不会设置服务器,而是会将 Ajax 调用替换为测试替身。对于时间示例,我们将使用测试替身来允许我们“向前移动时间”。

这听起来可能有点奇怪,但基本概念很简单。因为 JavaScript 非常动态,所以我们可以获取任何函数并将其替换为其他内容。测试替身只是将这个想法更进一步。使用 Sinon,我们可以用测试替身替换任何 JavaScript 函数,然后可以对其进行配置以执行各种操作,使测试复杂的事情变得简单。

Sinon 将测试替身分为三种类型:

  • 间谍,提供有关函数调用的信息,而不会影响其行为
  • 存根,就像间谍一样,但完全替换了函数。这使得可以使存根函数执行任何您喜欢的事情——抛出异常、返回特定值等
  • 模拟,通过组合间谍和存根,使替换整个对象更容易

此外,Sinon 还提供了一些其他帮助程序,尽管这些帮助程序不在本文的讨论范围之内:

  • 假计时器,可用于向前移动时间,例如触发 setTimeout
  • 假 XMLHttpRequest 和服务器,可用于伪造 Ajax 请求和响应

凭借这些功能,Sinon 允许您解决外部依赖项在测试中导致的所有难题。如果您学习有效使用 Sinon 的技巧,则不需要任何其他工具。

安装 Sinon

首先,我们需要安装 Sinon。

对于 Node.js 测试:

  1. 使用 npm install sinon 通过 npm 安装 Sinon
  2. 使用 var sinon = require('sinon'); 在您的测试中引入 Sinon

对于基于浏览器的测试:

  1. 您可以使用 npm install sinon 通过 npm 安装 Sinon,使用 CDN 或从 Sinon 的网站下载它
  2. 在您的测试运行程序页面中包含 sinon.js。

入门

Sinon 具有许多功能,但其中许多功能都是建立在其自身之上的。您了解一部分,就已经了解了下一部分。一旦您了解了基础知识并了解了每个不同部分的作用,这就会使 Sinon 易于使用。

当我们的代码调用给我们带来麻烦的函数时,我们通常需要 Sinon。

对于 Ajax,它可能是 $.get 或 XMLHttpRequest。对于时间,该函数可能是 setTimeout。对于数据库,它可能是 mongodb.findOne。

为了更容易讨论此函数,我将其称为 依赖项。我们正在测试的函数 依赖 于另一个函数的结果。

我们可以说,Sinon 的基本使用模式是用测试替身替换有问题的依赖项。

  • 在测试 Ajax 时,我们将 XMLHttpRequest 替换为一个模拟 Ajax 请求的测试替身
  • 在测试时间时,我们将 setTimeout 替换为一个假计时器
  • 在测试数据库访问时,我们可以将 mongodb.findOne 替换为一个立即返回一些假数据的测试替身

让我们看看它在实践中是如何工作的。

间谍

间谍是 Sinon 最简单的部分,其他功能建立在其之上。

间谍的主要用途是收集有关函数调用的信息。您还可以使用它们来帮助验证某些事情,例如是否调用了函数。

<code class="language-javascript">var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
</code>

函数 sinon.spy 返回一个 Spy 对象,可以像函数一样调用它,但还包含有关对其进行的任何调用的信息的属性。在上面的示例中,firstCall 属性包含有关第一次调用的信息,例如 firstCall.args,它是传递的参数列表。

尽管您可以通过不带参数地调用 sinon.spy 来创建匿名间谍,但更常见的模式是用间谍替换另一个函数。

<code class="language-javascript">var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
</code>

用间谍替换另一个函数的工作方式与前面的示例类似,但有一个重要的区别:完成使用间谍后,务必记住恢复原始函数,如上面示例的最后一行所示。如果没有这个,您的测试可能会行为异常。

间谍有很多不同的属性,这些属性提供了关于它们如何使用的不同信息。Sinon 的间谍文档包含所有可用选项的完整列表。

在实践中,您可能不会经常使用间谍。您更有可能需要一个存根,但间谍可能很方便,例如验证是否调用了回调:

<code class="language-javascript">function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});
</code>

在这个例子中,我使用 Mocha 作为测试框架,使用 Chai 作为断言库。如果您想了解更多关于这两个的信息,请参考我之前的文章:使用 Mocha 和 Chai 对您的 JavaScript 进行单元测试。

Sinon 的断言

在我们继续讨论存根之前,让我们快速绕道一下,看看 Sinon 的断言。

在大多数使用间谍(和存根)的测试情况下,您需要某种方法来验证测试的结果。

我们可以使用任何类型的断言来验证结果。在前面关于回调的示例中,我们使用了 Chai 的 assert 函数,它确保该值是真值。

<code class="language-javascript">var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
</code>

这样做的缺点是失败时的错误消息不清楚。您只会收到“false 不是 true”或类似的提示。正如您可能想象的那样,这在找出问题所在方面帮助不大,您需要查看测试的源代码才能弄清楚。不好玩。

为了解决这个问题,我们可以将自定义错误消息包含到断言中。

<code class="language-javascript">var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
</code>

但是,当我们可以使用 Sinon 自身的断言 时,为什么要费心呢?

<code class="language-javascript">function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});
</code>

像这样使用 Sinon 的断言可以立即提供更好的错误消息。当您需要验证更复杂的条件(例如函数的参数)时,这会非常有用。

以下是 Sinon 提供的其他一些有用断言的示例:

  • sinon.assert.calledWith 可用于验证是否使用特定参数调用了函数(这可能是我最常用的一个)
  • sinon.assert.callOrder 可以验证函数是否按特定顺序调用

与间谍一样,Sinon 的断言文档包含所有可用的选项。如果您喜欢使用 Chai,还有一个 sinon-chai 插件可用,它允许您通过 Chai 的 expect 或 should 接口使用 Sinon 断言。

存根

存根是首选的测试替身,因为它们灵活且方便。它们具有间谍的所有功能,但它们不仅仅是监视函数的作用,存根完全替换了它。换句话说,使用间谍时,原始函数仍然运行,但使用存根时,它不会运行。

这使得存根非常适合许多任务,例如:

  • 替换使测试编写缓慢且困难的 Ajax 或其他外部调用
  • 根据函数输出触发不同的代码路径
  • 测试异常情况,例如抛出异常时会发生什么?

我们可以创建存根的方式与间谍类似……

<code class="language-javascript">assert(callback.calledOnce);
</code>

我们可以像间谍一样创建匿名存根,但是当您使用存根替换现有函数时,存根会变得非常有用。

例如,如果我们有一些使用 jQuery 的 Ajax 功能的代码,则测试它很困难。代码会向我们配置的任何服务器发送请求,因此我们需要使它可用,或者向代码添加一个特殊情况,以便不在测试环境中执行此操作——这是一个很大的禁忌。您几乎不应该在代码中包含特定于测试的情况。

与其求助于不良做法,我们可以使用 Sinon 并将 Ajax 功能替换为存根。这使得测试它变得微不足道。

这是一个我们将测试的示例函数。它将对象作为参数,并通过 Ajax 将其发送到预定义的 URL。

<code class="language-javascript">var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
</code>

通常,由于 Ajax 调用和预定义的 URL,测试这将很困难,但如果我们使用存根,它就会变得很容易。

假设我们要确保传递给 saveUser 的回调函数在请求完成后正确调用。

<code class="language-javascript">var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
</code>

在这里,我们用存根替换 Ajax 函数。这意味着请求永远不会发送,我们不需要服务器或任何东西——我们完全控制测试代码中发生的事情!

因为我们想确保我们传递给 saveUser 的回调被调用,所以我们将指示存根 yield。这意味着存根会自动调用作为参数传递给它的第一个函数。这模拟了 $.post 的行为,它会在请求完成后调用回调。

除了存根之外,我们还在此测试中创建了一个间谍。我们可以使用普通函数作为回调,但使用间谍可以很容易地使用 Sinon 的 sinon.assert.calledOnce 断言来验证测试的结果。

在大多数情况下,当您需要存根时,您可以遵循相同的基本模式:

  • 找到有问题的函数,例如 $.post
  • 查看它的工作方式,以便您可以在测试中模拟它
  • 创建一个存根
  • 将存根设置为在测试中具有所需的行为

存根不需要模拟每种行为。测试所需的唯一行为是必要的,其他任何东西都可以省略。

存根的另一个常见用法是验证是否使用特定参数集调用了函数。

例如,对于我们的 Ajax 功能,我们要确保发送正确的值。因此,我们可以有类似以下内容:

<code class="language-javascript">function myFunction(condition, callback){
  if(condition){
    callback();
  }
}

describe('myFunction', function() {
  it('should call the callback function', function() {
    var callback = sinon.spy();

    myFunction(true, callback);

    assert(callback.calledOnce);
  });
});
</code>

同样,我们为 $.post() 创建了一个存根,但这次我们没有将其设置为 yield。此测试不关心回调,因此让它 yield 是不必要的。

我们设置了一些变量来包含预期数据——URL 和参数。设置这样的变量是一个好习惯,因为它可以让我们一目了然地看到测试的要求。它还可以帮助我们设置用户变量而无需重复值。

这次我们使用了 sinon.assert.calledWith() 断言。我们将存根作为它的第一个参数传递,因为这次我们要验证存根是否使用正确的参数调用。

还有另一种在 Sinon 中测试 Ajax 请求的方法。这是通过使用 Sinon 的假 XMLHttpRequest 功能来实现的。我们不会在这里详细介绍它,但如果您想了解它的工作原理,请参阅我关于使用 Sinon 的假 XMLHttpRequest 进行 Ajax 测试的文章。

模拟

模拟是一种与存根不同的方法。如果您听说过“模拟对象”这个术语,这就是同样的意思——Sinon 的模拟可以用来替换整个对象并改变它们的行为,类似于存根函数。

如果您需要从单个对象存根多个函数,它们主要是有用的。如果您只需要替换单个函数,则存根更容易使用。

使用模拟时,您应该小心!由于它们的强大功能,很容易使您的测试过于具体——测试过多且过于具体的事情——这可能会使您的测试无意中变得脆弱。

与间谍和存根不同,模拟具有内置断言。您可以预先定义预期结果,方法是告诉模拟对象需要发生什么,然后在测试结束时调用验证函数。

假设我们使用 store.js 将内容保存到 localStorage,并且我们想测试与之相关的函数。我们可以使用模拟来帮助测试它,如下所示:

<code class="language-javascript">var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
</code>

使用模拟时,我们使用流畅的调用样式定义预期的调用及其结果,如上所示。这与使用断言验证测试结果相同,只是我们预先定义了它们,并且为了验证它们,我们在测试结束时调用 storeMock.verify()。

在 Sinon 的模拟对象术语中,调用 mock.expects('something') 会创建一个 期望。也就是说,方法 mock.something() 预计会被调用。每个期望除了模拟特定功能外,还支持与间谍和存根相同的函数。

您可能会发现,使用存根通常比使用模拟更容易——这完全没问题。应该谨慎使用模拟。

有关模拟特定函数的完整列表,请查看 Sinon 的模拟文档。

重要的最佳实践:使用 sinon.test()

Sinon 有一个重要的最佳实践,在使用间谍、存根或模拟时应记住。

如果您用测试替身替换现有函数,请使用 sinon.test()。

在前面的示例中,我们使用 stub.restore() 或 mock.restore() 来清理使用它们后的内容。这是必要的,因为否则测试替身将保留在原处,并且可能会对其他测试产生负面影响或导致错误。

但是直接使用 restore() 函数是有问题的。被测试的函数可能会导致错误,并在调用 restore() 之前结束测试函数!

我们有两种方法可以解决这个问题:我们可以将整个内容包装在一个 try catch 块中。这允许我们将 restore() 调用放在 finally 块中,确保无论发生什么都会运行它。

或者,更好的方法是使用 sinon.test() 包装测试函数

<code class="language-javascript">var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
</code>

在上面的示例中,请注意 it() 的第二个参数包装在 sinon.test() 中。需要注意的第二件事是,我们使用 this.stub() 而不是 sinon.stub()。

使用 sinon.test() 包装测试允许我们使用 Sinon 的 沙箱 功能,允许我们通过 this.spy()、this.stub() 和 this.mock() 创建间谍、存根和模拟。使用沙箱创建的任何测试替身都会 自动 清理。

请注意,我们上面的示例代码没有 stub.restore()——由于测试被沙箱化,因此它是不必要的。

如果尽可能使用 sinon.test(),您可以避免由于早期测试由于错误而没有清理其测试替身而导致测试开始随机失败的问题。

Sinon 不是魔术

Sinon 执行许多操作,有时可能很难理解它的工作原理。让我们来看看 Sinon 工作原理的一些简单的 JavaScript 示例,以便我们可以更好地了解它的内部工作原理。这将帮助您在不同的情况下更有效地使用它。

我们也可以手动创建间谍、存根和模拟。我们使用 Sinon 的原因是它使任务变得微不足道——手动创建它们可能非常复杂,但让我们看看它是如何工作的,以了解 Sinon 的作用。

首先,间谍本质上是一个函数包装器:

<code class="language-javascript">var spy = sinon.spy();

//我们可以像函数一样调用间谍
spy('Hello', 'World');

//现在我们可以获取有关调用的信息
console.log(spy.firstCall.args); //输出:['Hello', 'World']
</code>

我们可以很容易地使用自定义函数获得间谍功能。但是请注意,Sinon 的间谍提供了更广泛的功能——包括断言支持。这使得 Sinon 更方便。

那存根呢?

要创建一个非常简单的存根,您可以简单地用一个新函数替换一个函数:

<code class="language-javascript">var user = {
  ...
  setName: function(name){
    this.name = name;
  }
}

//为 setName 函数创建一个间谍
var setNameSpy = sinon.spy(user, 'setName');

//现在,每当我们调用该函数时,间谍都会记录有关它的信息
user.setName('Darth Vader');

//我们可以通过查看间谍对象来查看
console.log(setNameSpy.callCount); //输出:1

//重要最后一步 - 删除间谍
setNameSpy.restore();
</code>

但是,Sinon 的存根提供了几个优点:

  • 它们包含完整的间谍功能
  • 您可以使用 stub.restore() 轻松恢复原始行为
  • 您可以针对 Sinon 存根进行断言

模拟只是结合了间谍和存根的行为,从而可以以不同的方式使用它们的功能。

即使 Sinon 有时看起来像是做了很多“魔术”,在大多数情况下,这也可以很容易地用您自己的代码来完成。Sinon 使用起来要方便得多,而不是必须为该目的编写自己的库。

结论

测试现实生活中的代码有时似乎过于复杂,很容易完全放弃。但是借助 Sinon,测试几乎任何类型的代码都变得轻而易举。

只需记住主要原则:如果函数使您的测试难以编写,请尝试将其替换为测试替身。无论函数执行什么操作,此原则都适用。

想要了解如何在您自己的代码中应用 Sinon?访问我的网站,我会向您发送我的免费现实世界中的 Sinon 指南,其中包括 Sinon 最佳实践以及如何在不同类型的测试情况下应用它的三个现实世界示例!

关于 Sinon.js 测试的常见问题解答 (FAQ)

Sinon.js 中的模拟、间谍和存根有什么区别?

在 Sinon.js 中,模拟、间谍和存根具有不同的用途。间谍是记录所有调用的参数、返回值、this 的值和抛出的异常(如果有)的函数。它们可用于跟踪函数调用和响应。存根类似于间谍,但具有预编程行为。它们还会记录有关如何调用它们的信息,但与间谍不同,它们可用于控制方法的行为,以强制方法抛出错误或返回特定值。模拟是具有预编程行为(如存根)以及预编程期望的假方法(如间谍)。

如何在 JavaScript 中使用 Sinon.js 进行单元测试?

Sinon.js 是一个强大的工具,用于在 JavaScript 测试中创建间谍、存根和模拟。要使用它,您首先需要将其包含在您的项目中,方法是在您的 HTML 中使用脚本标记或通过 npm 安装它。包含后,您可以使用其 API 创建和管理间谍、存根和模拟。然后,这些可以用于隔离您正在测试的代码并确保它按预期运行。

如何在 Sinon.js 中创建间谍?

在 Sinon.js 中创建间谍很简单。您只需调用 sinon.spy() 函数。这将返回一个间谍函数,您可以在测试中使用它。间谍将记录有关如何调用它的信息,然后您可以在测试中检查这些信息。例如,您可以检查调用间谍的次数、使用什么参数调用它以及它返回的内容。

如何在 Sinon.js 中创建存根?

要在 Sinon.js 中创建存根,您需要调用 sinon.stub() 函数。这将返回一个存根函数,您可以在测试中使用它。存根的行为类似于间谍,记录有关如何调用它的信息,但它还允许您控制其行为。例如,您可以使存根抛出错误或返回特定值。

如何在 Sinon.js 中创建模拟?

在 Sinon.js 中创建模拟涉及调用 sinon.mock() 函数。这将返回一个模拟对象,您可以在测试中使用它。模拟对象的行为类似于间谍,记录有关如何调用它的信息,并且类似于存根,允许您控制其行为。但它还允许您设置有关如何调用它的期望。

如何将 Sinon.js 与其他测试框架一起使用?

Sinon.js 旨在与任何 JavaScript 测试框架一起使用。它提供了一个独立的测试框架,但它也可以与 Mocha、Jasmine 和 QUnit 等其他流行的测试框架集成。Sinon.js 文档提供了与这些框架和其他测试框架集成的示例。

如何将存根或间谍恢复到其原始函数?

如果您已用存根或间谍替换了函数,则可以通过调用存根或间谍上的 .restore() 方法来恢复原始函数。如果您想在测试后进行清理,以确保存根或间谍不会影响其他测试,这将非常有用。

如何检查是否使用特定参数调用了间谍?

Sinon.js 提供了几种方法来检查如何调用间谍。例如,您可以使用 .calledWith() 方法来检查是否使用特定参数调用了间谍。您还可以使用 .calledOnceWith() 方法来检查间谍是否只使用特定参数调用了一次。

如何使存根返回特定值?

您可以使用 .returns() 方法使存根返回特定值。例如,如果您有一个名为 myStub 的存根,则可以通过调用 myStub.returns('foo') 使其返回值 'foo'。

如何使存根抛出错误?

您可以使用 .throws() 方法使存根抛出错误。例如,如果您有一个名为 myStub 的存根,则可以通过调用 myStub.throws() 使其抛出错误。默认情况下,这将抛出一个 Error 对象,但您也可以通过将错误的名称作为参数传递来使其抛出特定类型的错误。

以上是Sinon教程:使用模拟,间谍和存根的JavaScript测试的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn