六、轻量级单元测试

第 4 章异步测试-AJAX中,我们已经看到了在应用中包含 AJAX 测试会如何增加测试的复杂性。在示例中,我们创建了一个结果可预测的服务器。这基本上是一个复杂的测试装置。即使我们可以使用一个真实的服务器实现,它也会增加测试的复杂性,尝试通过浏览器使用数据库或第三方服务来改变服务器的状态,这不是一个容易或可扩展的解决方案。

还有对生产力的影响;这些请求需要时间来处理和传输,这损害了单元测试通常提供的快速反馈循环。

你也可以说这些规范同时测试客户端和服务器代码,因此不能被认为是单元测试,而是集成测试。

所有这些问题的解决方案是使用存根赝品来代替代码的真正依赖关系。因此,我们不是向服务器发出请求,而是在浏览器内部使用服务器的测试替身。

我们将使用第 4 章异步测试-AJAX中的同一个例子,并使用不同的技术重写它。

Jasmine 花根

我们已经看到了 Jasmine 间谍的一些用例。记住间谍是一个特殊的功能记录它是如何被调用的。你可以把存根想象成一个有行为的间谍。

每当我们想要在我们的规范中强制一个特定的路径,或者用一个更简单的实现替换一个真实的实现时,我们都使用 Stubs。

让我们通过使用 Jasmine Stubs 重写接受标准的例子“股票在被提取时,应该更新它的股价”。

因为我们知道股票的fetch功能是使用$.getJSON功能实现的:

Stock.prototype.fetch = function(parameters) {
  $.getJSON(url, function (data) {
    that.sharePrice = data.sharePrice;
    success(that);
  });
};

我们可以使用spyOn功能在getJSON功能上设置一个间谍:

describe("when fetched", function() {
  beforeEach(function() {
    spyOn($, 'getJSON').andCallFake(function(url, callback) {
      callback({ sharePrice: 20.13 });
    });
    stock.fetch();
  });

  it("should update its share price", function() {
    expect(stock.sharePrice).toEqual(20.13);
  });
});

但是这次我们将使用andCallFake功能为我们的间谍设置一个行为(默认情况下,间谍什么也不做,返回未定义的)。我们让间谍用对象响应调用其callback参数({ sharePrice: 20.13 })。

后来,在预期下,我们用toEqual断言来验证股票的sharePrice发生了变化。

为了运行这个规范,您不再需要一个服务器来发出请求,这是一件好事,但是这种方法有一个问题。如果提取功能被重构为使用$.ajax而不是$.getJSON,那么测试将失败。西农提供了更好的解决方案。JS 库,是 Stub 浏览器的 AJAX 基础设施,所以 AJAX 请求的实现是自由的,可以用不同的方式完成。

否则。联署材料

西农。JS 是一个伟大的库,由克里斯蒂安·约翰森创建,他是伟大的书测试驱动的 JavaScript 开发的作者,目的是使处理存根、间谍和模仿变得容易。

尽管 Jasmine 已经支持 Stubs 和 Spies,但我们将使用 Sinon 的特定功能。JS 以测试 AJAX 请求,其FakeXMLHttpRequestFakeServer功能。

正如您将在FakeXMLHttpRequest对象中看到的,存根和赝品之间的主要区别在于,赝品就像是真实组件的更简单但仍然完整的实现,并且它通常是在系统级别设置的。

否则安装。联署材料

在我们挖掘到规范实现之前,首先我们需要添加 Sinon。JS 来了这个项目。去http://sinonjs.org/下载当前版本,放在lib文件夹里。

我们还需要将其添加到SpecRunner.html文件中,所以继续添加另一个脚本:

<script type="text/javascript" src="lib/sinon.js"></script>

一个假的圣诞任务

每当你用 jQuery 发出 AJAX 请求时,在 T2 的引擎盖下,它会使用 T0 来实际执行请求。

XMLHttpRequest 是标准的 JavaScript HTTP API。尽管它的名字表明它使用了 XML,但它支持其他类型的内容,如 JSON,出于兼容性的原因,这个名字保持不变。

所以我们可以用假的,全局的XMLHttpRequest对象来代替存根 jQuery。这正是西农。JS 通过其FakeXMLHttpRequest实现做到了这一点。

让我们重写之前的规范来使用这个 Fake 实现:

describe("when fetched", function() {
  var xhr;

  beforeEach(function() {
    var fetchRequest;
    xhr = sinon.useFakeXMLHttpRequest();

    xhr.onCreate = function (request) {
      fetchRequest = request;
    };

    stock.fetch();

    fetchRequest.respond(
      200,
      { "Content-Type": "application/json" },
      '{ "sharePrice": 20.13 }'
    );
  });

  afterEach(function() {
    xhr.restore();
  });

  it("should update its share price", function() {
    expect(stock.sharePrice).toEqual(20.13);
  });
});

首先,我们告诉西农。JS 使用sinon.useFakeXMLHttpRequest函数用它的 Fake 替换原来的实现。

然后,我们添加一个观察器,通过设置一个函数作为xhr.onCreate属性的值来获取新创建的请求,并将它们存储在一个名为fetchRequest的变量中。

然后我们调用stock.fetch函数,它将调用$.getJSON,在引擎盖下创建一个新的XMLHttpRequest

最后,我们使用fetchRequest变量(包含观察者捕捉到的FakeXMLHttpRequest对象,用一个假的内容来回应。

我们使用respond功能,它接受三个参数:

  • 定义 HTTP 状态代码的整数
  • 包含 HTTP 标头的对象
  • 带有响应正文的字符串

然后,这是一个运行期望的问题:

it("should update its share price", function() {
  expect(stock.sharePrice).toEqual(20.13);
});

从西农开始。JS 改变全局XMLHttpRequest对象,你一定要记得告诉 Sinon。JS to 在测试运行后将其恢复到原始实现,否则您可能会干扰来自其他规范的代码(例如 Jasmine jQuery fights 模块):

afterEach(function() {
  xhr.restore();
});

假服务器

西农。JS 的FakeXMLHttpRequest是一个非常好的存根 AJAX 请求的解决方案,但是如果你需要处理多个请求,或者需要对不同的请求有不同的响应,事情可能会开始变得复杂。

为了帮助管理实例,西农。JS 附带了另一个解决方案,Fake 服务器。

西农。JS Fake 服务器将单个FakeXMLHttpRequest实例的操作抽象成一个高级 API,让您专注于特定请求类型的响应。

同样,让我们重写同一个示例,但是现在使用 Fake 服务器功能:

describe("when fetched", function() {
  var xhr;

  beforeEach(function() {
    xhr = sinon.fakeServer.create();
    xhr.respondWith([
      200,
      { "Content-Type": "application/json" },
      '{ "sharePrice": 20.13 }'
    ]);

    stock.fetch();

    xhr.respond();
  });
  afterEach(function() {
    xhr.restore();
  });

  it("should update its share price", function() {
    expect(stock.sharePrice).toEqual(20.13);
  });
});

现在,不再处理XMLHttpRequest,而是使用sinon.fakeServer.create函数创建一个新的假服务器实例。

然后,我们调用respondWith函数来配置 Fake 服务器,使其总是用 Fake 响应来响应请求。

stock.fetch() 呼叫之后,我们告诉 Fake 服务器响应所有发出的请求。

每次规范运行后,恢复原来的XMLHttpRequest行为也很重要。

假服务器最酷的地方是它能够根据不同的网址创建不同的响应。例如,我们可以将之前的服务器响应写成:

xhr.respondWith(
  '/stocks/AOUE',
  [
    200,
    { "Content-Type": "application/json" },
    '{ "sharePrice": 20.13 }'
  ]
);

请注意额外的参数'/stocks/AOUE'告诉假服务器只响应用该网址发出的请求。甚至可以指定 HTTP 方法(GET、POST 等),并使用正则表达式来匹配 URL:

xhr.respondWith(
  'GET',
  /\/stocks\/(.+)/,
  [
    200,
    { "Content-Type": "application/json" },
    '{ "sharePrice": 20.13 }'
  ]
);

您还可以将函数传递给身体参数,并获得动态响应:

xhr.respondWith(
  'GET',
  /\/stocks\/(.+)/,
  function (request, stockSymbol) {
    request.respond(
      200,
      { "Content-Type": "application/json" },
      '{ "sharePrice": 20.13 }'
    );
  }
);

注意stockSymbol参数,它包含基于/\/stocks\/(.+)/正则表达式从请求网址中提取的匹配值。每当使用常规的表达式和函数体来处理假的服务器上的请求时,匹配的字符串会按照它们被找到的顺序传递给函数。

总结

在这一章中,您已经了解了异步测试是如何损害单元测试的快速反馈循环的。我展示了如何使用 Stubs 或 Fakes 来使你的规范运行得更快,依赖更少。

我们已经看到了测试 AJAX 请求的三种不同方式,一种是简单的 Jasmine Stub,另一种是更高级的 Fake XMLHttpRequest,还有来自 Sinon.JS 库的 Fake 服务器。

您还对间谍和存根更加熟悉,在不同的场景中使用它们应该会更舒服。

在下一章中,我们将进一步探讨我们的应用的复杂性,我们将进行整体重构,将其转换为功能齐全的单页应用Backbone.js